From 8579430db94bd79c19a685e9745ec1c002b27b0e Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Fri, 15 Mar 2024 11:55:13 +0100 Subject: [PATCH 01/35] wip: migrate to mono-repo. SPN has already been moved to spn/ --- .ci-inject-internal-deps.sh | 22 - Gopkg.lock | 405 -------- Gopkg.toml | 35 - assets/.gitkeep | 0 cmds/hub/.gitignore | 3 + cmds/hub/build | 60 ++ cmds/hub/main.go | 66 ++ cmds/hub/pack | 123 +++ cmds/integrationtest/netstate.go | 6 +- cmds/observation-hub/.gitignore | 3 + cmds/observation-hub/Dockerfile | 38 + cmds/observation-hub/apprise.go | 257 +++++ cmds/observation-hub/apprise_test.go | 84 ++ cmds/observation-hub/build | 60 ++ cmds/observation-hub/main.go | 44 + cmds/observation-hub/notifications.tmpl | 75 ++ cmds/observation-hub/observe.go | 407 ++++++++ cmds/portmaster-core/main.go | 14 +- cmds/portmaster-start/main.go | 2 +- cmds/portmaster-start/recover_linux.go | 2 +- cmds/portmaster-start/run.go | 2 +- cmds/portmaster-start/show.go | 2 +- cmds/portmaster-start/update.go | 2 +- cmds/portmaster-start/verify.go | 2 +- cmds/testsuite/.gitignore | 3 + cmds/testsuite/db.go | 33 + cmds/testsuite/login.go | 125 +++ cmds/testsuite/main.go | 69 ++ cmds/testsuite/report_healthcheck.go | 51 + cmds/winkext-test/main.go | 5 +- desktop/angular/.gitkeep | 0 desktop/tauri/.gitkeep | 0 go.mod | 9 +- go.sum | 4 + pack | 3 + packaging/linux/.gitkeep | 0 packaging/windows/.gitkeep | 0 runtime/.gitkeep | 1 + {broadcasts => service/broadcasts}/api.go | 0 {broadcasts => service/broadcasts}/data.go | 12 +- .../broadcasts}/install_info.go | 0 {broadcasts => service/broadcasts}/module.go | 0 {broadcasts => service/broadcasts}/notify.go | 2 +- {broadcasts => service/broadcasts}/state.go | 0 .../broadcasts}/testdata/README.md | 0 .../broadcasts}/testdata/notifications.yaml | 0 {compat => service/compat}/api.go | 0 {compat => service/compat}/callbacks.go | 4 +- {compat => service/compat}/debug_default.go | 0 {compat => service/compat}/debug_linux.go | 0 {compat => service/compat}/debug_windows.go | 0 {compat => service/compat}/iptables.go | 0 {compat => service/compat}/iptables_test.go | 0 {compat => service/compat}/module.go | 4 +- {compat => service/compat}/notify.go | 4 +- {compat => service/compat}/selfcheck.go | 6 +- {compat => service/compat}/wfpstate.go | 0 {compat => service/compat}/wfpstate_test.go | 0 {core => service/core}/api.go | 12 +- {core => service/core}/base/databases.go | 0 {core => service/core}/base/global.go | 0 {core => service/core}/base/logs.go | 0 {core => service/core}/base/module.go | 0 {core => service/core}/base/profiling.go | 0 {core => service/core}/config.go | 0 {core => service/core}/core.go | 14 +- {core => service/core}/os_default.go | 0 {core => service/core}/os_windows.go | 0 {core => service/core}/pmtesting/testing.go | 4 +- {detection => service/detection}/dga/lms.go | 0 .../detection}/dga/lms_test.go | 0 {firewall => service/firewall}/api.go | 10 +- {firewall => service/firewall}/bypassing.go | 10 +- {firewall => service/firewall}/config.go | 4 +- {firewall => service/firewall}/dns.go | 10 +- .../firewall}/inspection/inspection.go | 4 +- .../interception/ebpf/bandwidth/bpf_bpfeb.go | 0 .../interception/ebpf/bandwidth/bpf_bpfeb.o | Bin .../interception/ebpf/bandwidth/bpf_bpfel.go | 0 .../interception/ebpf/bandwidth/bpf_bpfel.o | Bin .../interception/ebpf/bandwidth/interface.go | 2 +- .../ebpf/connection_listener/bpf_bpfeb.go | 0 .../ebpf/connection_listener/bpf_bpfeb.o | Bin .../ebpf/connection_listener/bpf_bpfel.go | 0 .../ebpf/connection_listener/bpf_bpfel.o | Bin .../ebpf/connection_listener/worker.go | 2 +- .../interception/ebpf/exec/bpf_bpfeb.go | 0 .../interception/ebpf/exec/bpf_bpfeb.o | Bin .../interception/ebpf/exec/bpf_bpfel.go | 0 .../interception/ebpf/exec/bpf_bpfel.o | Bin .../firewall}/interception/ebpf/exec/exec.go | 0 .../interception/ebpf/programs/bandwidth.c | 0 .../ebpf/programs/bpf/bpf_core_read.h | 0 .../ebpf/programs/bpf/bpf_helper_defs.h | 0 .../ebpf/programs/bpf/bpf_helpers.h | 0 .../ebpf/programs/bpf/bpf_tracing.h | 0 .../interception/ebpf/programs/exec.c | 0 .../interception/ebpf/programs/monitor.c | 0 .../interception/ebpf/programs/update.sh | 0 .../interception/ebpf/programs/vmlinux-x86.h | 0 .../interception/interception_default.go | 4 +- .../interception/interception_linux.go | 10 +- .../interception/interception_windows.go | 8 +- .../firewall}/interception/introspection.go | 0 .../firewall}/interception/module.go | 2 +- .../firewall}/interception/nfq/conntrack.go | 4 +- .../firewall}/interception/nfq/nfq.go | 4 +- .../firewall}/interception/nfq/packet.go | 2 +- .../firewall}/interception/nfqueue_linux.go | 6 +- .../firewall}/interception/packet_tracer.go | 2 +- .../windowskext/bandwidth_stats.go | 2 +- .../firewall}/interception/windowskext/doc.go | 0 .../interception/windowskext/handler.go | 6 +- .../interception/windowskext/kext.go | 4 +- .../interception/windowskext/packet.go | 4 +- .../interception/windowskext/service.go | 0 .../interception/windowskext/syscall.go | 0 {firewall => service/firewall}/master.go | 18 +- {firewall => service/firewall}/module.go | 10 +- .../firewall}/packet_handler.go | 22 +- {firewall => service/firewall}/preauth.go | 8 +- {firewall => service/firewall}/prompt.go | 8 +- {firewall => service/firewall}/tunnel.go | 24 +- {intel => service/intel}/block_reason.go | 2 +- .../intel}/customlists/config.go | 0 {intel => service/intel}/customlists/lists.go | 2 +- .../intel}/customlists/module.go | 0 {intel => service/intel}/entity.go | 6 +- {intel => service/intel}/filterlists/bloom.go | 0 .../intel}/filterlists/cache_version.go | 0 .../intel}/filterlists/database.go | 2 +- .../intel}/filterlists/decoder.go | 0 {intel => service/intel}/filterlists/index.go | 2 +- {intel => service/intel}/filterlists/keys.go | 0 .../intel}/filterlists/lookup.go | 0 .../intel}/filterlists/module.go | 4 +- .../intel}/filterlists/module_test.go | 0 .../intel}/filterlists/record.go | 0 .../intel}/filterlists/updater.go | 0 .../intel}/geoip/country_info.go | 0 .../intel}/geoip/country_info_test.go | 0 {intel => service/intel}/geoip/database.go | 2 +- {intel => service/intel}/geoip/location.go | 0 .../intel}/geoip/location_test.go | 0 {intel => service/intel}/geoip/lookup.go | 0 {intel => service/intel}/geoip/lookup_test.go | 0 {intel => service/intel}/geoip/module.go | 2 +- {intel => service/intel}/geoip/module_test.go | 2 +- {intel => service/intel}/geoip/regions.go | 0 .../intel}/geoip/regions_test.go | 0 {intel => service/intel}/module.go | 2 +- {intel => service/intel}/resolver.go | 0 {nameserver => service/nameserver}/config.go | 2 +- .../nameserver}/conflict.go | 4 +- {nameserver => service/nameserver}/failing.go | 4 +- {nameserver => service/nameserver}/metrics.go | 0 {nameserver => service/nameserver}/module.go | 6 +- .../nameserver}/nameserver.go | 12 +- .../nameserver}/nsutil/nsutil.go | 0 .../nameserver}/response.go | 2 +- {netenv => service/netenv}/addresses_test.go | 0 {netenv => service/netenv}/adresses.go | 2 +- {netenv => service/netenv}/api.go | 0 {netenv => service/netenv}/dbus_linux.go | 0 {netenv => service/netenv}/dbus_linux_test.go | 0 {netenv => service/netenv}/dialing.go | 0 {netenv => service/netenv}/environment.go | 0 .../netenv}/environment_default.go | 0 .../netenv}/environment_linux.go | 2 +- .../netenv}/environment_linux_test.go | 0 .../netenv}/environment_test.go | 0 .../netenv}/environment_windows.go | 0 .../netenv}/environment_windows_test.go | 0 {netenv => service/netenv}/icmp_listener.go | 2 +- {netenv => service/netenv}/location.go | 6 +- .../netenv}/location_default.go | 0 {netenv => service/netenv}/location_test.go | 0 .../netenv}/location_windows.go | 0 {netenv => service/netenv}/main.go | 0 {netenv => service/netenv}/main_test.go | 2 +- {netenv => service/netenv}/network-change.go | 0 {netenv => service/netenv}/notes.md | 0 {netenv => service/netenv}/online-status.go | 4 +- .../netenv}/online-status_test.go | 0 {netenv => service/netenv}/os_android.go | 3 +- {netenv => service/netenv}/os_default.go | 0 .../netquery}/active_chart_handler.go | 2 +- .../netquery}/bandwidth_chart_handler.go | 2 +- {netquery => service/netquery}/database.go | 10 +- {netquery => service/netquery}/manager.go | 2 +- {netquery => service/netquery}/module_api.go | 2 +- {netquery => service/netquery}/orm/decoder.go | 0 .../netquery}/orm/decoder_test.go | 0 {netquery => service/netquery}/orm/encoder.go | 0 .../netquery}/orm/encoder_test.go | 0 .../netquery}/orm/query_runner.go | 0 .../netquery}/orm/schema_builder.go | 0 .../netquery}/orm/schema_builder_test.go | 0 {netquery => service/netquery}/query.go | 2 +- .../netquery}/query_handler.go | 2 +- .../netquery}/query_request.go | 2 +- {netquery => service/netquery}/query_test.go | 2 +- .../netquery}/runtime_query_runner.go | 2 +- {network => service/network}/api.go | 10 +- {network => service/network}/api_test.go | 2 +- {network => service/network}/clean.go | 6 +- {network => service/network}/connection.go | 20 +- .../network}/connection_android.go | 10 +- .../network}/connection_store.go | 0 {network => service/network}/database.go | 2 +- {network => service/network}/dns.go | 8 +- {network => service/network}/iphelper/get.go | 2 +- .../network}/iphelper/iphelper.go | 0 .../network}/iphelper/tables.go | 2 +- .../network}/iphelper/tables_test.go | 0 {network => service/network}/metrics.go | 2 +- {network => service/network}/module.go | 6 +- {network => service/network}/multicast.go | 2 +- .../network}/netutils/address.go | 2 +- {network => service/network}/netutils/dns.go | 0 .../network}/netutils/dns_test.go | 0 {network => service/network}/netutils/ip.go | 0 .../network}/netutils/ip_test.go | 0 .../network}/netutils/tcpassembly.go | 0 .../network}/packet/bandwidth.go | 0 {network => service/network}/packet/const.go | 0 .../network}/packet/info_only.go | 0 {network => service/network}/packet/packet.go | 0 .../network}/packet/packetinfo.go | 0 {network => service/network}/packet/parse.go | 0 {network => service/network}/ports.go | 0 {network => service/network}/proc/findpid.go | 2 +- .../network}/proc/pids_by_user.go | 0 {network => service/network}/proc/tables.go | 2 +- .../network}/proc/tables_test.go | 0 .../network}/reference/ports.go | 0 .../network}/reference/protocols.go | 0 {network => service/network}/socket/socket.go | 0 {network => service/network}/state/exists.go | 4 +- {network => service/network}/state/info.go | 4 +- {network => service/network}/state/lookup.go | 6 +- .../network}/state/system_default.go | 2 +- .../network}/state/system_linux.go | 4 +- .../network}/state/system_windows.go | 4 +- {network => service/network}/state/tcp.go | 2 +- {network => service/network}/state/udp.go | 6 +- {network => service/network}/status.go | 0 {process => service/process}/api.go | 2 +- {process => service/process}/config.go | 0 {process => service/process}/database.go | 2 +- {process => service/process}/doc.go | 0 {process => service/process}/executable.go | 0 {process => service/process}/find.go | 8 +- {process => service/process}/module.go | 2 +- {process => service/process}/module_test.go | 2 +- {process => service/process}/process.go | 2 +- .../process}/process_default.go | 0 {process => service/process}/process_linux.go | 0 .../process}/process_windows.go | 0 {process => service/process}/profile.go | 2 +- {process => service/process}/special.go | 4 +- {process => service/process}/tags.go | 2 +- .../process}/tags/appimage_unix.go | 6 +- .../process}/tags/flatpak_unix.go | 6 +- .../process}/tags/interpreter_unix.go | 6 +- {process => service/process}/tags/net.go | 4 +- .../process}/tags/snap_unix.go | 6 +- .../process}/tags/svchost_windows.go | 6 +- .../process}/tags/winstore_windows.go | 6 +- {profile => service/profile}/active.go | 0 {profile => service/profile}/api.go | 2 +- .../profile}/binmeta/convert.go | 0 .../profile}/binmeta/find_default.go | 0 .../profile}/binmeta/find_linux.go | 0 .../profile}/binmeta/find_linux_test.go | 0 .../profile}/binmeta/find_windows.go | 0 .../profile}/binmeta/find_windows_test.go | 0 {profile => service/profile}/binmeta/icon.go | 0 {profile => service/profile}/binmeta/icons.go | 0 .../profile}/binmeta/locations_linux.go | 0 {profile => service/profile}/binmeta/name.go | 0 .../profile}/binmeta/name_test.go | 0 {profile => service/profile}/config-update.go | 4 +- {profile => service/profile}/config.go | 6 +- {profile => service/profile}/database.go | 0 .../profile}/endpoints/annotations.go | 0 .../profile}/endpoints/endpoint-any.go | 2 +- .../profile}/endpoints/endpoint-asn.go | 2 +- .../profile}/endpoints/endpoint-continent.go | 2 +- .../profile}/endpoints/endpoint-country.go | 2 +- .../profile}/endpoints/endpoint-domain.go | 4 +- .../profile}/endpoints/endpoint-ip.go | 2 +- .../profile}/endpoints/endpoint-iprange.go | 2 +- .../profile}/endpoints/endpoint-lists.go | 2 +- .../profile}/endpoints/endpoint-scopes.go | 4 +- .../profile}/endpoints/endpoint.go | 4 +- .../profile}/endpoints/endpoint_test.go | 0 .../profile}/endpoints/endpoints.go | 2 +- .../profile}/endpoints/endpoints_test.go | 4 +- .../profile}/endpoints/reason.go | 0 {profile => service/profile}/fingerprint.go | 0 .../profile}/fingerprint_test.go | 0 {profile => service/profile}/framework.go | 0 .../profile}/framework_test.go | 0 {profile => service/profile}/get.go | 0 {profile => service/profile}/merge.go | 2 +- {profile => service/profile}/meta.go | 0 {profile => service/profile}/migrations.go | 2 +- {profile => service/profile}/module.go | 6 +- .../profile}/profile-layered-provider.go | 0 .../profile}/profile-layered.go | 4 +- {profile => service/profile}/profile.go | 6 +- {profile => service/profile}/special.go | 0 {resolver => service/resolver}/api.go | 0 .../resolver}/block-detection.go | 0 {resolver => service/resolver}/compat.go | 0 {resolver => service/resolver}/config.go | 6 +- {resolver => service/resolver}/doc.go | 0 {resolver => service/resolver}/failing.go | 2 +- {resolver => service/resolver}/ipinfo.go | 0 {resolver => service/resolver}/ipinfo_test.go | 0 {resolver => service/resolver}/main.go | 6 +- {resolver => service/resolver}/main_test.go | 2 +- {resolver => service/resolver}/metrics.go | 0 {resolver => service/resolver}/namerecord.go | 0 .../resolver}/namerecord_test.go | 0 {resolver => service/resolver}/resolve.go | 2 +- .../resolver}/resolver-env.go | 4 +- .../resolver}/resolver-https.go | 2 +- .../resolver}/resolver-mdns.go | 4 +- .../resolver}/resolver-plain.go | 2 +- .../resolver}/resolver-tcp.go | 2 +- {resolver => service/resolver}/resolver.go | 4 +- .../resolver}/resolver_test.go | 0 {resolver => service/resolver}/resolvers.go | 4 +- .../resolver}/resolvers_test.go | 0 {resolver => service/resolver}/reverse.go | 0 .../resolver}/reverse_test.go | 0 {resolver => service/resolver}/rr_context.go | 0 {resolver => service/resolver}/rrcache.go | 4 +- .../resolver}/rrcache_test.go | 0 {resolver => service/resolver}/scopes.go | 2 +- .../resolver}/test/resolving.bash | 0 {status => service/status}/module.go | 2 +- {status => service/status}/provider.go | 2 +- {status => service/status}/records.go | 2 +- {status => service/status}/security_level.go | 0 {sync => service/sync}/module.go | 0 {sync => service/sync}/profile.go | 4 +- {sync => service/sync}/setting_single.go | 2 +- {sync => service/sync}/settings.go | 2 +- {sync => service/sync}/util.go | 0 {ui => service/ui}/api.go | 0 {ui => service/ui}/module.go | 0 {ui => service/ui}/serve.go | 2 +- {updates => service/updates}/api.go | 0 .../updates}/assets/portmaster.service | 0 {updates => service/updates}/config.go | 2 +- {updates => service/updates}/export.go | 2 +- {updates => service/updates}/get.go | 2 +- .../updates}/helper/electron.go | 0 .../updates}/helper/indexes.go | 0 .../updates}/helper/signing.go | 0 .../updates}/helper/updates.go | 0 {updates => service/updates}/main.go | 2 +- {updates => service/updates}/notify.go | 0 .../updates}/os_integration_default.go | 0 .../updates}/os_integration_linux.go | 0 {updates => service/updates}/restart.go | 0 {updates => service/updates}/state.go | 0 {updates => service/updates}/upgrader.go | 2 +- spn/TESTING.md | 26 + spn/TRADEMARKS | 5 + spn/access/account/auth.go | 65 ++ spn/access/account/client.go | 14 + spn/access/account/types.go | 137 +++ spn/access/account/view.go | 123 +++ spn/access/api.go | 168 ++++ spn/access/client.go | 550 +++++++++++ spn/access/client_test.go | 79 ++ spn/access/database.go | 258 +++++ spn/access/features.go | 127 +++ spn/access/module.go | 194 ++++ spn/access/module_test.go | 13 + spn/access/notify.go | 105 ++ spn/access/op_auth.go | 75 ++ spn/access/storage.go | 131 +++ spn/access/token/errors.go | 15 + spn/access/token/module_test.go | 13 + spn/access/token/pblind.go | 552 +++++++++++ spn/access/token/pblind_gen_test.go | 39 + spn/access/token/pblind_test.go | 260 +++++ spn/access/token/registry.go | 116 +++ spn/access/token/request.go | 244 +++++ spn/access/token/request_test.go | 125 +++ spn/access/token/scramble.go | 240 +++++ spn/access/token/scramble_gen_test.go | 48 + spn/access/token/scramble_test.go | 84 ++ spn/access/token/token.go | 83 ++ spn/access/token/token_test.go | 33 + spn/access/zones.go | 257 +++++ spn/cabin/config-public.go | 392 ++++++++ spn/cabin/database.go | 98 ++ spn/cabin/identity.go | 311 ++++++ spn/cabin/identity_test.go | 129 +++ spn/cabin/keys.go | 179 ++++ spn/cabin/keys_test.go | 43 + spn/cabin/module.go | 26 + spn/cabin/module_test.go | 13 + spn/cabin/verification.go | 157 +++ spn/cabin/verification_test.go | 127 +++ spn/captain/api.go | 68 ++ spn/captain/bootstrap.go | 152 +++ spn/captain/client.go | 506 ++++++++++ spn/captain/config.go | 253 +++++ spn/captain/establish.go | 105 ++ spn/captain/exceptions.go | 28 + spn/captain/gossip.go | 38 + spn/captain/hooks.go | 47 + spn/captain/intel.go | 108 +++ spn/captain/module.go | 219 +++++ spn/captain/navigation.go | 306 ++++++ spn/captain/op_gossip.go | 156 +++ spn/captain/op_gossip_query.go | 195 ++++ spn/captain/op_publish.go | 183 ++++ spn/captain/piers.go | 131 +++ spn/captain/public.go | 247 +++++ spn/captain/status.go | 154 +++ spn/conf/map.go | 17 + spn/conf/mode.go | 30 + spn/conf/networks.go | 110 +++ spn/conf/version.go | 9 + spn/crew/connect.go | 482 +++++++++ spn/crew/metrics.go | 223 +++++ spn/crew/module.go | 44 + spn/crew/module_test.go | 13 + spn/crew/op_connect.go | 585 +++++++++++ spn/crew/op_connect_test.go | 115 +++ spn/crew/op_ping.go | 149 +++ spn/crew/op_ping_test.go | 32 + spn/crew/policy.go | 51 + spn/crew/sticky.go | 176 ++++ spn/docks/bandwidth_test.go | 90 ++ spn/docks/controller.go | 100 ++ spn/docks/crane.go | 913 ++++++++++++++++++ spn/docks/crane_establish.go | 81 ++ spn/docks/crane_init.go | 339 +++++++ spn/docks/crane_netstate.go | 131 +++ spn/docks/crane_terminal.go | 122 +++ spn/docks/crane_test.go | 267 +++++ spn/docks/crane_verify.go | 85 ++ spn/docks/cranehooks.go | 46 + spn/docks/hub_import.go | 189 ++++ spn/docks/measurements.go | 108 +++ spn/docks/metrics.go | 404 ++++++++ spn/docks/module.go | 117 +++ spn/docks/module_test.go | 16 + spn/docks/op_capacity.go | 356 +++++++ spn/docks/op_capacity_test.go | 85 ++ spn/docks/op_expand.go | 393 ++++++++ spn/docks/op_latency.go | 298 ++++++ spn/docks/op_latency_test.go | 59 ++ spn/docks/op_sync_state.go | 150 +++ spn/docks/op_whoami.go | 135 +++ spn/docks/op_whoami_test.go | 24 + spn/docks/terminal_expansion.go | 150 +++ spn/docks/terminal_expansion_test.go | 305 ++++++ spn/hub/database.go | 202 ++++ spn/hub/errors.go | 21 + spn/hub/format.go | 69 ++ spn/hub/format_test.go | 81 ++ spn/hub/hub.go | 435 +++++++++ spn/hub/hub_test.go | 79 ++ spn/hub/intel.go | 191 ++++ spn/hub/intel_override.go | 17 + spn/hub/measurements.go | 231 +++++ spn/hub/status.go | 308 ++++++ spn/hub/transport.go | 152 +++ spn/hub/transport_test.go | 147 +++ spn/hub/truststores.go | 17 + spn/hub/update.go | 524 ++++++++++ spn/hub/update_test.go | 70 ++ spn/navigator/api.go | 672 +++++++++++++ spn/navigator/api_route.go | 396 ++++++++ spn/navigator/costs.go | 72 ++ spn/navigator/database.go | 164 ++++ spn/navigator/findnearest.go | 441 +++++++++ spn/navigator/findnearest_test.go | 124 +++ spn/navigator/findroutes.go | 234 +++++ spn/navigator/findroutes_test.go | 54 ++ spn/navigator/intel.go | 222 +++++ spn/navigator/map.go | 165 ++++ spn/navigator/map_stats.go | 85 ++ spn/navigator/map_test.go | 279 ++++++ spn/navigator/measurements.go | 144 +++ spn/navigator/metrics.go | 177 ++++ spn/navigator/module.go | 129 +++ spn/navigator/module_test.go | 13 + spn/navigator/optimize.go | 388 ++++++++ spn/navigator/optimize_region.go | 224 +++++ spn/navigator/optimize_test.go | 188 ++++ spn/navigator/options.go | 330 +++++++ spn/navigator/pin.go | 269 ++++++ spn/navigator/pin_export.go | 98 ++ spn/navigator/region.go | 231 +++++ spn/navigator/route.go | 221 +++++ spn/navigator/routing-profiles.go | 162 ++++ spn/navigator/sort.go | 141 +++ spn/navigator/sort_test.go | 112 +++ spn/navigator/state.go | 426 ++++++++ spn/navigator/state_test.go | 31 + spn/navigator/testdata/main-intel.yml | 234 +++++ spn/navigator/update.go | 776 +++++++++++++++ spn/patrol/domains.go | 311 ++++++ spn/patrol/domains_test.go | 67 ++ spn/patrol/http.go | 186 ++++ spn/patrol/module.go | 32 + spn/ships/connection_test.go | 131 +++ spn/ships/http.go | 230 +++++ spn/ships/http_info.go | 83 ++ spn/ships/http_info_page.html.tmpl | 112 +++ spn/ships/http_info_test.go | 26 + spn/ships/http_shared.go | 188 ++++ spn/ships/http_shared_test.go | 33 + spn/ships/kcp.go | 81 ++ spn/ships/launch.go | 114 +++ spn/ships/masking.go | 63 ++ spn/ships/module.go | 20 + spn/ships/mtu.go | 47 + spn/ships/pier.go | 82 ++ spn/ships/registry.go | 55 ++ spn/ships/ship.go | 220 +++++ spn/ships/tcp.go | 145 +++ spn/ships/testship.go | 154 +++ spn/ships/testship_test.go | 58 ++ spn/ships/virtual_network.go | 43 + spn/sluice/module.go | 46 + spn/sluice/packet_listener.go | 277 ++++++ spn/sluice/request.go | 78 ++ spn/sluice/sluice.go | 229 +++++ spn/sluice/sluices.go | 47 + spn/sluice/udp_listener.go | 334 +++++++ spn/spn.go | 1 + spn/terminal/control_flow.go | 454 +++++++++ spn/terminal/defaults.go | 36 + spn/terminal/errors.go | 221 +++++ spn/terminal/fmt.go | 27 + spn/terminal/init.go | 210 ++++ spn/terminal/metrics.go | 117 +++ spn/terminal/module.go | 80 ++ spn/terminal/module_test.go | 13 + spn/terminal/msg.go | 106 ++ spn/terminal/msgtypes.go | 66 ++ spn/terminal/operation.go | 332 +++++++ spn/terminal/operation_base.go | 185 ++++ spn/terminal/operation_counter.go | 255 +++++ spn/terminal/permission.go | 50 + spn/terminal/rate_limit.go | 39 + spn/terminal/session.go | 166 ++++ spn/terminal/session_test.go | 94 ++ spn/terminal/terminal.go | 909 +++++++++++++++++ spn/terminal/terminal_test.go | 311 ++++++ spn/terminal/testing.go | 243 +++++ spn/terminal/upstream.go | 16 + spn/test | 168 ++++ spn/tools/Dockerfile | 23 + spn/tools/container-init.sh | 30 + spn/tools/install.sh | 326 +++++++ spn/tools/start-checksum.txt | 1 + spn/tools/sysctl.conf | 45 + spn/unit/doc.go | 13 + spn/unit/scheduler.go | 358 +++++++ spn/unit/scheduler_stats.go | 87 ++ spn/unit/scheduler_test.go | 51 + spn/unit/unit.go | 103 ++ spn/unit/unit_debug.go | 86 ++ spn/unit/unit_test.go | 104 ++ 577 files changed, 35981 insertions(+), 818 deletions(-) delete mode 100755 .ci-inject-internal-deps.sh delete mode 100644 Gopkg.lock delete mode 100644 Gopkg.toml create mode 100644 assets/.gitkeep create mode 100644 cmds/hub/.gitignore create mode 100755 cmds/hub/build create mode 100644 cmds/hub/main.go create mode 100755 cmds/hub/pack create mode 100644 cmds/observation-hub/.gitignore create mode 100644 cmds/observation-hub/Dockerfile create mode 100644 cmds/observation-hub/apprise.go create mode 100644 cmds/observation-hub/apprise_test.go create mode 100755 cmds/observation-hub/build create mode 100644 cmds/observation-hub/main.go create mode 100644 cmds/observation-hub/notifications.tmpl create mode 100644 cmds/observation-hub/observe.go create mode 100644 cmds/testsuite/.gitignore create mode 100644 cmds/testsuite/db.go create mode 100644 cmds/testsuite/login.go create mode 100644 cmds/testsuite/main.go create mode 100644 cmds/testsuite/report_healthcheck.go create mode 100644 desktop/angular/.gitkeep create mode 100644 desktop/tauri/.gitkeep create mode 100644 packaging/linux/.gitkeep create mode 100644 packaging/windows/.gitkeep create mode 100644 runtime/.gitkeep rename {broadcasts => service/broadcasts}/api.go (100%) rename {broadcasts => service/broadcasts}/data.go (91%) rename {broadcasts => service/broadcasts}/install_info.go (100%) rename {broadcasts => service/broadcasts}/module.go (100%) rename {broadcasts => service/broadcasts}/notify.go (99%) rename {broadcasts => service/broadcasts}/state.go (100%) rename {broadcasts => service/broadcasts}/testdata/README.md (100%) rename {broadcasts => service/broadcasts}/testdata/notifications.yaml (100%) rename {compat => service/compat}/api.go (100%) rename {compat => service/compat}/callbacks.go (90%) rename {compat => service/compat}/debug_default.go (100%) rename {compat => service/compat}/debug_linux.go (100%) rename {compat => service/compat}/debug_windows.go (100%) rename {compat => service/compat}/iptables.go (100%) rename {compat => service/compat}/iptables_test.go (100%) rename {compat => service/compat}/module.go (97%) rename {compat => service/compat}/notify.go (98%) rename {compat => service/compat}/selfcheck.go (97%) rename {compat => service/compat}/wfpstate.go (100%) rename {compat => service/compat}/wfpstate_test.go (100%) rename {core => service/core}/api.go (96%) rename {core => service/core}/base/databases.go (100%) rename {core => service/core}/base/global.go (100%) rename {core => service/core}/base/logs.go (100%) rename {core => service/core}/base/module.go (100%) rename {core => service/core}/base/profiling.go (100%) rename {core => service/core}/config.go (100%) rename {core => service/core}/core.go (84%) rename {core => service/core}/os_default.go (100%) rename {core => service/core}/os_windows.go (100%) rename {core => service/core}/pmtesting/testing.go (96%) rename {detection => service/detection}/dga/lms.go (100%) rename {detection => service/detection}/dga/lms_test.go (100%) rename {firewall => service/firewall}/api.go (96%) rename {firewall => service/firewall}/bypassing.go (87%) rename {firewall => service/firewall}/config.go (98%) rename {firewall => service/firewall}/dns.go (97%) rename {firewall => service/firewall}/inspection/inspection.go (95%) rename {firewall => service/firewall}/interception/ebpf/bandwidth/bpf_bpfeb.go (100%) rename {firewall => service/firewall}/interception/ebpf/bandwidth/bpf_bpfeb.o (100%) rename {firewall => service/firewall}/interception/ebpf/bandwidth/bpf_bpfel.go (100%) rename {firewall => service/firewall}/interception/ebpf/bandwidth/bpf_bpfel.o (100%) rename {firewall => service/firewall}/interception/ebpf/bandwidth/interface.go (98%) rename {firewall => service/firewall}/interception/ebpf/connection_listener/bpf_bpfeb.go (100%) rename {firewall => service/firewall}/interception/ebpf/connection_listener/bpf_bpfeb.o (100%) rename {firewall => service/firewall}/interception/ebpf/connection_listener/bpf_bpfel.go (100%) rename {firewall => service/firewall}/interception/ebpf/connection_listener/bpf_bpfel.o (100%) rename {firewall => service/firewall}/interception/ebpf/connection_listener/worker.go (98%) rename {firewall => service/firewall}/interception/ebpf/exec/bpf_bpfeb.go (100%) rename {firewall => service/firewall}/interception/ebpf/exec/bpf_bpfeb.o (100%) rename {firewall => service/firewall}/interception/ebpf/exec/bpf_bpfel.go (100%) rename {firewall => service/firewall}/interception/ebpf/exec/bpf_bpfel.o (100%) rename {firewall => service/firewall}/interception/ebpf/exec/exec.go (100%) rename {firewall => service/firewall}/interception/ebpf/programs/bandwidth.c (100%) rename {firewall => service/firewall}/interception/ebpf/programs/bpf/bpf_core_read.h (100%) rename {firewall => service/firewall}/interception/ebpf/programs/bpf/bpf_helper_defs.h (100%) rename {firewall => service/firewall}/interception/ebpf/programs/bpf/bpf_helpers.h (100%) rename {firewall => service/firewall}/interception/ebpf/programs/bpf/bpf_tracing.h (100%) rename {firewall => service/firewall}/interception/ebpf/programs/exec.c (100%) rename {firewall => service/firewall}/interception/ebpf/programs/monitor.c (100%) rename {firewall => service/firewall}/interception/ebpf/programs/update.sh (100%) rename {firewall => service/firewall}/interception/ebpf/programs/vmlinux-x86.h (100%) rename {firewall => service/firewall}/interception/interception_default.go (87%) rename {firewall => service/firewall}/interception/interception_linux.go (77%) rename {firewall => service/firewall}/interception/interception_windows.go (88%) rename {firewall => service/firewall}/interception/introspection.go (100%) rename {firewall => service/firewall}/interception/module.go (96%) rename {firewall => service/firewall}/interception/nfq/conntrack.go (97%) rename {firewall => service/firewall}/interception/nfq/nfq.go (98%) rename {firewall => service/firewall}/interception/nfq/packet.go (98%) rename {firewall => service/firewall}/interception/nfqueue_linux.go (98%) rename {firewall => service/firewall}/interception/packet_tracer.go (95%) rename {firewall => service/firewall}/interception/windowskext/bandwidth_stats.go (98%) rename {firewall => service/firewall}/interception/windowskext/doc.go (100%) rename {firewall => service/firewall}/interception/windowskext/handler.go (97%) rename {firewall => service/firewall}/interception/windowskext/kext.go (98%) rename {firewall => service/firewall}/interception/windowskext/packet.go (97%) rename {firewall => service/firewall}/interception/windowskext/service.go (100%) rename {firewall => service/firewall}/interception/windowskext/syscall.go (100%) rename {firewall => service/firewall}/master.go (97%) rename {firewall => service/firewall}/module.go (94%) rename {firewall => service/firewall}/packet_handler.go (97%) rename {firewall => service/firewall}/preauth.go (93%) rename {firewall => service/firewall}/prompt.go (97%) rename {firewall => service/firewall}/tunnel.go (91%) rename {intel => service/intel}/block_reason.go (97%) rename {intel => service/intel}/customlists/config.go (100%) rename {intel => service/intel}/customlists/lists.go (98%) rename {intel => service/intel}/customlists/module.go (100%) rename {intel => service/intel}/entity.go (98%) rename {intel => service/intel}/filterlists/bloom.go (100%) rename {intel => service/intel}/filterlists/cache_version.go (100%) rename {intel => service/intel}/filterlists/database.go (99%) rename {intel => service/intel}/filterlists/decoder.go (100%) rename {intel => service/intel}/filterlists/index.go (99%) rename {intel => service/intel}/filterlists/keys.go (100%) rename {intel => service/intel}/filterlists/lookup.go (100%) rename {intel => service/intel}/filterlists/module.go (96%) rename {intel => service/intel}/filterlists/module_test.go (100%) rename {intel => service/intel}/filterlists/record.go (100%) rename {intel => service/intel}/filterlists/updater.go (100%) rename {intel => service/intel}/geoip/country_info.go (100%) rename {intel => service/intel}/geoip/country_info_test.go (100%) rename {intel => service/intel}/geoip/database.go (98%) rename {intel => service/intel}/geoip/location.go (100%) rename {intel => service/intel}/geoip/location_test.go (100%) rename {intel => service/intel}/geoip/lookup.go (100%) rename {intel => service/intel}/geoip/lookup_test.go (100%) rename {intel => service/intel}/geoip/module.go (94%) rename {intel => service/intel}/geoip/module_test.go (64%) rename {intel => service/intel}/geoip/regions.go (100%) rename {intel => service/intel}/geoip/regions_test.go (100%) rename {intel => service/intel}/module.go (82%) rename {intel => service/intel}/resolver.go (100%) rename {nameserver => service/nameserver}/config.go (97%) rename {nameserver => service/nameserver}/conflict.go (94%) rename {nameserver => service/nameserver}/failing.go (97%) rename {nameserver => service/nameserver}/metrics.go (100%) rename {nameserver => service/nameserver}/module.go (97%) rename {nameserver => service/nameserver}/nameserver.go (97%) rename {nameserver => service/nameserver}/nsutil/nsutil.go (100%) rename {nameserver => service/nameserver}/response.go (97%) rename {netenv => service/netenv}/addresses_test.go (100%) rename {netenv => service/netenv}/adresses.go (98%) rename {netenv => service/netenv}/api.go (100%) rename {netenv => service/netenv}/dbus_linux.go (100%) rename {netenv => service/netenv}/dbus_linux_test.go (100%) rename {netenv => service/netenv}/dialing.go (100%) rename {netenv => service/netenv}/environment.go (100%) rename {netenv => service/netenv}/environment_default.go (100%) rename {netenv => service/netenv}/environment_linux.go (98%) rename {netenv => service/netenv}/environment_linux_test.go (100%) rename {netenv => service/netenv}/environment_test.go (100%) rename {netenv => service/netenv}/environment_windows.go (100%) rename {netenv => service/netenv}/environment_windows_test.go (100%) rename {netenv => service/netenv}/icmp_listener.go (98%) rename {netenv => service/netenv}/location.go (98%) rename {netenv => service/netenv}/location_default.go (100%) rename {netenv => service/netenv}/location_test.go (100%) rename {netenv => service/netenv}/location_windows.go (100%) rename {netenv => service/netenv}/main.go (100%) rename {netenv => service/netenv}/main_test.go (65%) rename {netenv => service/netenv}/network-change.go (100%) rename {netenv => service/netenv}/notes.md (100%) rename {netenv => service/netenv}/online-status.go (99%) rename {netenv => service/netenv}/online-status_test.go (100%) rename {netenv => service/netenv}/os_android.go (92%) rename {netenv => service/netenv}/os_default.go (100%) rename {netquery => service/netquery}/active_chart_handler.go (98%) rename {netquery => service/netquery}/bandwidth_chart_handler.go (98%) rename {netquery => service/netquery}/database.go (98%) rename {netquery => service/netquery}/manager.go (99%) rename {netquery => service/netquery}/module_api.go (99%) rename {netquery => service/netquery}/orm/decoder.go (100%) rename {netquery => service/netquery}/orm/decoder_test.go (100%) rename {netquery => service/netquery}/orm/encoder.go (100%) rename {netquery => service/netquery}/orm/encoder_test.go (100%) rename {netquery => service/netquery}/orm/query_runner.go (100%) rename {netquery => service/netquery}/orm/schema_builder.go (100%) rename {netquery => service/netquery}/orm/schema_builder_test.go (100%) rename {netquery => service/netquery}/query.go (99%) rename {netquery => service/netquery}/query_handler.go (99%) rename {netquery => service/netquery}/query_request.go (99%) rename {netquery => service/netquery}/query_test.go (98%) rename {netquery => service/netquery}/runtime_query_runner.go (97%) rename {network => service/network}/api.go (97%) rename {network => service/network}/api_test.go (98%) rename {network => service/network}/clean.go (95%) rename {network => service/network}/connection.go (98%) rename {network => service/network}/connection_android.go (88%) rename {network => service/network}/connection_store.go (100%) rename {network => service/network}/database.go (98%) rename {network => service/network}/dns.go (97%) rename {network => service/network}/iphelper/get.go (96%) rename {network => service/network}/iphelper/iphelper.go (100%) rename {network => service/network}/iphelper/tables.go (99%) rename {network => service/network}/iphelper/tables_test.go (100%) rename {network => service/network}/metrics.go (98%) rename {network => service/network}/module.go (96%) rename {network => service/network}/multicast.go (96%) rename {network => service/network}/netutils/address.go (96%) rename {network => service/network}/netutils/dns.go (100%) rename {network => service/network}/netutils/dns_test.go (100%) rename {network => service/network}/netutils/ip.go (100%) rename {network => service/network}/netutils/ip_test.go (100%) rename {network => service/network}/netutils/tcpassembly.go (100%) rename {network => service/network}/packet/bandwidth.go (100%) rename {network => service/network}/packet/const.go (100%) rename {network => service/network}/packet/info_only.go (100%) rename {network => service/network}/packet/packet.go (100%) rename {network => service/network}/packet/packetinfo.go (100%) rename {network => service/network}/packet/parse.go (100%) rename {network => service/network}/ports.go (100%) rename {network => service/network}/proc/findpid.go (97%) rename {network => service/network}/proc/pids_by_user.go (100%) rename {network => service/network}/proc/tables.go (99%) rename {network => service/network}/proc/tables_test.go (100%) rename {network => service/network}/reference/ports.go (100%) rename {network => service/network}/reference/protocols.go (100%) rename {network => service/network}/socket/socket.go (100%) rename {network => service/network}/state/exists.go (95%) rename {network => service/network}/state/info.go (89%) rename {network => service/network}/state/lookup.go (97%) rename {network => service/network}/state/system_default.go (95%) rename {network => service/network}/state/system_linux.go (90%) rename {network => service/network}/state/system_windows.go (80%) rename {network => service/network}/state/tcp.go (97%) rename {network => service/network}/state/udp.go (97%) rename {network => service/network}/status.go (100%) rename {process => service/process}/api.go (98%) rename {process => service/process}/config.go (100%) rename {process => service/process}/database.go (98%) rename {process => service/process}/doc.go (100%) rename {process => service/process}/executable.go (100%) rename {process => service/process}/find.go (95%) rename {process => service/process}/module.go (91%) rename {process => service/process}/module_test.go (65%) rename {process => service/process}/process.go (99%) rename {process => service/process}/process_default.go (100%) rename {process => service/process}/process_linux.go (100%) rename {process => service/process}/process_windows.go (100%) rename {process => service/process}/profile.go (98%) rename {process => service/process}/special.go (96%) rename {process => service/process}/tags.go (97%) rename {process => service/process}/tags/appimage_unix.go (96%) rename {process => service/process}/tags/flatpak_unix.go (92%) rename {process => service/process}/tags/interpreter_unix.go (97%) rename {process => service/process}/tags/net.go (93%) rename {process => service/process}/tags/snap_unix.go (95%) rename {process => service/process}/tags/svchost_windows.go (95%) rename {process => service/process}/tags/winstore_windows.go (95%) rename {profile => service/profile}/active.go (100%) rename {profile => service/profile}/api.go (98%) rename {profile => service/profile}/binmeta/convert.go (100%) rename {profile => service/profile}/binmeta/find_default.go (100%) rename {profile => service/profile}/binmeta/find_linux.go (100%) rename {profile => service/profile}/binmeta/find_linux_test.go (100%) rename {profile => service/profile}/binmeta/find_windows.go (100%) rename {profile => service/profile}/binmeta/find_windows_test.go (100%) rename {profile => service/profile}/binmeta/icon.go (100%) rename {profile => service/profile}/binmeta/icons.go (100%) rename {profile => service/profile}/binmeta/locations_linux.go (100%) rename {profile => service/profile}/binmeta/name.go (100%) rename {profile => service/profile}/binmeta/name_test.go (100%) rename {profile => service/profile}/config-update.go (96%) rename {profile => service/profile}/config.go (99%) rename {profile => service/profile}/database.go (100%) rename {profile => service/profile}/endpoints/annotations.go (100%) rename {profile => service/profile}/endpoints/endpoint-any.go (92%) rename {profile => service/profile}/endpoints/endpoint-asn.go (96%) rename {profile => service/profile}/endpoints/endpoint-continent.go (96%) rename {profile => service/profile}/endpoints/endpoint-country.go (96%) rename {profile => service/profile}/endpoints/endpoint-domain.go (97%) rename {profile => service/profile}/endpoints/endpoint-ip.go (94%) rename {profile => service/profile}/endpoints/endpoint-iprange.go (94%) rename {profile => service/profile}/endpoints/endpoint-lists.go (95%) rename {profile => service/profile}/endpoints/endpoint-scopes.go (95%) rename {profile => service/profile}/endpoints/endpoint.go (98%) rename {profile => service/profile}/endpoints/endpoint_test.go (100%) rename {profile => service/profile}/endpoints/endpoints.go (98%) rename {profile => service/profile}/endpoints/endpoints_test.go (98%) rename {profile => service/profile}/endpoints/reason.go (100%) rename {profile => service/profile}/fingerprint.go (100%) rename {profile => service/profile}/fingerprint_test.go (100%) rename {profile => service/profile}/framework.go (100%) rename {profile => service/profile}/framework_test.go (100%) rename {profile => service/profile}/get.go (100%) rename {profile => service/profile}/merge.go (98%) rename {profile => service/profile}/meta.go (100%) rename {profile => service/profile}/migrations.go (99%) rename {profile => service/profile}/module.go (93%) rename {profile => service/profile}/profile-layered-provider.go (100%) rename {profile => service/profile}/profile-layered.go (99%) rename {profile => service/profile}/profile.go (98%) rename {profile => service/profile}/special.go (100%) rename {resolver => service/resolver}/api.go (100%) rename {resolver => service/resolver}/block-detection.go (100%) rename {resolver => service/resolver}/compat.go (100%) rename {resolver => service/resolver}/config.go (98%) rename {resolver => service/resolver}/doc.go (100%) rename {resolver => service/resolver}/failing.go (98%) rename {resolver => service/resolver}/ipinfo.go (100%) rename {resolver => service/resolver}/ipinfo_test.go (100%) rename {resolver => service/resolver}/main.go (97%) rename {resolver => service/resolver}/main_test.go (97%) rename {resolver => service/resolver}/metrics.go (100%) rename {resolver => service/resolver}/namerecord.go (100%) rename {resolver => service/resolver}/namerecord_test.go (100%) rename {resolver => service/resolver}/resolve.go (99%) rename {resolver => service/resolver}/resolver-env.go (97%) rename {resolver => service/resolver}/resolver-https.go (98%) rename {resolver => service/resolver}/resolver-mdns.go (99%) rename {resolver => service/resolver}/resolver-plain.go (98%) rename {resolver => service/resolver}/resolver-tcp.go (99%) rename {resolver => service/resolver}/resolver.go (98%) rename {resolver => service/resolver}/resolver_test.go (100%) rename {resolver => service/resolver}/resolvers.go (99%) rename {resolver => service/resolver}/resolvers_test.go (100%) rename {resolver => service/resolver}/reverse.go (100%) rename {resolver => service/resolver}/reverse_test.go (100%) rename {resolver => service/resolver}/rr_context.go (100%) rename {resolver => service/resolver}/rrcache.go (98%) rename {resolver => service/resolver}/rrcache_test.go (100%) rename {resolver => service/resolver}/scopes.go (99%) rename {resolver => service/resolver}/test/resolving.bash (100%) rename {status => service/status}/module.go (95%) rename {status => service/status}/provider.go (95%) rename {status => service/status}/records.go (92%) rename {status => service/status}/security_level.go (100%) rename {sync => service/sync}/module.go (100%) rename {sync => service/sync}/profile.go (99%) rename {sync => service/sync}/setting_single.go (99%) rename {sync => service/sync}/settings.go (99%) rename {sync => service/sync}/util.go (100%) rename {ui => service/ui}/api.go (100%) rename {ui => service/ui}/module.go (100%) rename {ui => service/ui}/serve.go (99%) rename {updates => service/updates}/api.go (100%) rename {updates => service/updates}/assets/portmaster.service (100%) rename {updates => service/updates}/config.go (99%) rename {updates => service/updates}/export.go (99%) rename {updates => service/updates}/get.go (97%) rename {updates => service/updates}/helper/electron.go (100%) rename {updates => service/updates}/helper/indexes.go (100%) rename {updates => service/updates}/helper/signing.go (100%) rename {updates => service/updates}/helper/updates.go (100%) rename {updates => service/updates}/main.go (99%) rename {updates => service/updates}/notify.go (100%) rename {updates => service/updates}/os_integration_default.go (100%) rename {updates => service/updates}/os_integration_linux.go (100%) rename {updates => service/updates}/restart.go (100%) rename {updates => service/updates}/state.go (100%) rename {updates => service/updates}/upgrader.go (99%) create mode 100644 spn/TESTING.md create mode 100644 spn/TRADEMARKS create mode 100644 spn/access/account/auth.go create mode 100644 spn/access/account/client.go create mode 100644 spn/access/account/types.go create mode 100644 spn/access/account/view.go create mode 100644 spn/access/api.go create mode 100644 spn/access/client.go create mode 100644 spn/access/client_test.go create mode 100644 spn/access/database.go create mode 100644 spn/access/features.go create mode 100644 spn/access/module.go create mode 100644 spn/access/module_test.go create mode 100644 spn/access/notify.go create mode 100644 spn/access/op_auth.go create mode 100644 spn/access/storage.go create mode 100644 spn/access/token/errors.go create mode 100644 spn/access/token/module_test.go create mode 100644 spn/access/token/pblind.go create mode 100644 spn/access/token/pblind_gen_test.go create mode 100644 spn/access/token/pblind_test.go create mode 100644 spn/access/token/registry.go create mode 100644 spn/access/token/request.go create mode 100644 spn/access/token/request_test.go create mode 100644 spn/access/token/scramble.go create mode 100644 spn/access/token/scramble_gen_test.go create mode 100644 spn/access/token/scramble_test.go create mode 100644 spn/access/token/token.go create mode 100644 spn/access/token/token_test.go create mode 100644 spn/access/zones.go create mode 100644 spn/cabin/config-public.go create mode 100644 spn/cabin/database.go create mode 100644 spn/cabin/identity.go create mode 100644 spn/cabin/identity_test.go create mode 100644 spn/cabin/keys.go create mode 100644 spn/cabin/keys_test.go create mode 100644 spn/cabin/module.go create mode 100644 spn/cabin/module_test.go create mode 100644 spn/cabin/verification.go create mode 100644 spn/cabin/verification_test.go create mode 100644 spn/captain/api.go create mode 100644 spn/captain/bootstrap.go create mode 100644 spn/captain/client.go create mode 100644 spn/captain/config.go create mode 100644 spn/captain/establish.go create mode 100644 spn/captain/exceptions.go create mode 100644 spn/captain/gossip.go create mode 100644 spn/captain/hooks.go create mode 100644 spn/captain/intel.go create mode 100644 spn/captain/module.go create mode 100644 spn/captain/navigation.go create mode 100644 spn/captain/op_gossip.go create mode 100644 spn/captain/op_gossip_query.go create mode 100644 spn/captain/op_publish.go create mode 100644 spn/captain/piers.go create mode 100644 spn/captain/public.go create mode 100644 spn/captain/status.go create mode 100644 spn/conf/map.go create mode 100644 spn/conf/mode.go create mode 100644 spn/conf/networks.go create mode 100644 spn/conf/version.go create mode 100644 spn/crew/connect.go create mode 100644 spn/crew/metrics.go create mode 100644 spn/crew/module.go create mode 100644 spn/crew/module_test.go create mode 100644 spn/crew/op_connect.go create mode 100644 spn/crew/op_connect_test.go create mode 100644 spn/crew/op_ping.go create mode 100644 spn/crew/op_ping_test.go create mode 100644 spn/crew/policy.go create mode 100644 spn/crew/sticky.go create mode 100644 spn/docks/bandwidth_test.go create mode 100644 spn/docks/controller.go create mode 100644 spn/docks/crane.go create mode 100644 spn/docks/crane_establish.go create mode 100644 spn/docks/crane_init.go create mode 100644 spn/docks/crane_netstate.go create mode 100644 spn/docks/crane_terminal.go create mode 100644 spn/docks/crane_test.go create mode 100644 spn/docks/crane_verify.go create mode 100644 spn/docks/cranehooks.go create mode 100644 spn/docks/hub_import.go create mode 100644 spn/docks/measurements.go create mode 100644 spn/docks/metrics.go create mode 100644 spn/docks/module.go create mode 100644 spn/docks/module_test.go create mode 100644 spn/docks/op_capacity.go create mode 100644 spn/docks/op_capacity_test.go create mode 100644 spn/docks/op_expand.go create mode 100644 spn/docks/op_latency.go create mode 100644 spn/docks/op_latency_test.go create mode 100644 spn/docks/op_sync_state.go create mode 100644 spn/docks/op_whoami.go create mode 100644 spn/docks/op_whoami_test.go create mode 100644 spn/docks/terminal_expansion.go create mode 100644 spn/docks/terminal_expansion_test.go create mode 100644 spn/hub/database.go create mode 100644 spn/hub/errors.go create mode 100644 spn/hub/format.go create mode 100644 spn/hub/format_test.go create mode 100644 spn/hub/hub.go create mode 100644 spn/hub/hub_test.go create mode 100644 spn/hub/intel.go create mode 100644 spn/hub/intel_override.go create mode 100644 spn/hub/measurements.go create mode 100644 spn/hub/status.go create mode 100644 spn/hub/transport.go create mode 100644 spn/hub/transport_test.go create mode 100644 spn/hub/truststores.go create mode 100644 spn/hub/update.go create mode 100644 spn/hub/update_test.go create mode 100644 spn/navigator/api.go create mode 100644 spn/navigator/api_route.go create mode 100644 spn/navigator/costs.go create mode 100644 spn/navigator/database.go create mode 100644 spn/navigator/findnearest.go create mode 100644 spn/navigator/findnearest_test.go create mode 100644 spn/navigator/findroutes.go create mode 100644 spn/navigator/findroutes_test.go create mode 100644 spn/navigator/intel.go create mode 100644 spn/navigator/map.go create mode 100644 spn/navigator/map_stats.go create mode 100644 spn/navigator/map_test.go create mode 100644 spn/navigator/measurements.go create mode 100644 spn/navigator/metrics.go create mode 100644 spn/navigator/module.go create mode 100644 spn/navigator/module_test.go create mode 100644 spn/navigator/optimize.go create mode 100644 spn/navigator/optimize_region.go create mode 100644 spn/navigator/optimize_test.go create mode 100644 spn/navigator/options.go create mode 100644 spn/navigator/pin.go create mode 100644 spn/navigator/pin_export.go create mode 100644 spn/navigator/region.go create mode 100644 spn/navigator/route.go create mode 100644 spn/navigator/routing-profiles.go create mode 100644 spn/navigator/sort.go create mode 100644 spn/navigator/sort_test.go create mode 100644 spn/navigator/state.go create mode 100644 spn/navigator/state_test.go create mode 100644 spn/navigator/testdata/main-intel.yml create mode 100644 spn/navigator/update.go create mode 100644 spn/patrol/domains.go create mode 100644 spn/patrol/domains_test.go create mode 100644 spn/patrol/http.go create mode 100644 spn/patrol/module.go create mode 100644 spn/ships/connection_test.go create mode 100644 spn/ships/http.go create mode 100644 spn/ships/http_info.go create mode 100644 spn/ships/http_info_page.html.tmpl create mode 100644 spn/ships/http_info_test.go create mode 100644 spn/ships/http_shared.go create mode 100644 spn/ships/http_shared_test.go create mode 100644 spn/ships/kcp.go create mode 100644 spn/ships/launch.go create mode 100644 spn/ships/masking.go create mode 100644 spn/ships/module.go create mode 100644 spn/ships/mtu.go create mode 100644 spn/ships/pier.go create mode 100644 spn/ships/registry.go create mode 100644 spn/ships/ship.go create mode 100644 spn/ships/tcp.go create mode 100644 spn/ships/testship.go create mode 100644 spn/ships/testship_test.go create mode 100644 spn/ships/virtual_network.go create mode 100644 spn/sluice/module.go create mode 100644 spn/sluice/packet_listener.go create mode 100644 spn/sluice/request.go create mode 100644 spn/sluice/sluice.go create mode 100644 spn/sluice/sluices.go create mode 100644 spn/sluice/udp_listener.go create mode 100644 spn/spn.go create mode 100644 spn/terminal/control_flow.go create mode 100644 spn/terminal/defaults.go create mode 100644 spn/terminal/errors.go create mode 100644 spn/terminal/fmt.go create mode 100644 spn/terminal/init.go create mode 100644 spn/terminal/metrics.go create mode 100644 spn/terminal/module.go create mode 100644 spn/terminal/module_test.go create mode 100644 spn/terminal/msg.go create mode 100644 spn/terminal/msgtypes.go create mode 100644 spn/terminal/operation.go create mode 100644 spn/terminal/operation_base.go create mode 100644 spn/terminal/operation_counter.go create mode 100644 spn/terminal/permission.go create mode 100644 spn/terminal/rate_limit.go create mode 100644 spn/terminal/session.go create mode 100644 spn/terminal/session_test.go create mode 100644 spn/terminal/terminal.go create mode 100644 spn/terminal/terminal_test.go create mode 100644 spn/terminal/testing.go create mode 100644 spn/terminal/upstream.go create mode 100755 spn/test create mode 100644 spn/tools/Dockerfile create mode 100755 spn/tools/container-init.sh create mode 100755 spn/tools/install.sh create mode 100644 spn/tools/start-checksum.txt create mode 100644 spn/tools/sysctl.conf create mode 100644 spn/unit/doc.go create mode 100644 spn/unit/scheduler.go create mode 100644 spn/unit/scheduler_stats.go create mode 100644 spn/unit/scheduler_test.go create mode 100644 spn/unit/unit.go create mode 100644 spn/unit/unit_debug.go create mode 100644 spn/unit/unit_test.go diff --git a/.ci-inject-internal-deps.sh b/.ci-inject-internal-deps.sh deleted file mode 100755 index 08304016..00000000 --- a/.ci-inject-internal-deps.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh - -DEP_FILE="Gopkg.toml" - -# remove ignored internal deps -sed -i '/ignored = \["github.com\/safing\//d' $DEP_FILE - -# portbase -PORTBASE_BRANCH="develop" -git branch | grep "* master" >/dev/null -if [ $? -eq 0 ]; then - PORTBASE_BRANCH="master" -fi -echo " -[[constraint]] - name = \"github.com/safing/portbase\" - branch = \"${PORTBASE_BRANCH}\" - -[[constraint]] - name = \"github.com/safing/spn\" - branch = \"${PORTBASE_BRANCH}\" -" >> $DEP_FILE diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index ebf28930..00000000 --- a/Gopkg.lock +++ /dev/null @@ -1,405 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - digest = "1:6146fda730c18186631e91e818d995e759e7cbe27644d6871ccd469f6865c686" - name = "github.com/StackExchange/wmi" - packages = ["."] - pruneopts = "" - revision = "cbe66965904dbe8a6cd589e2298e5d8b986bd7dd" - version = "1.1.0" - -[[projects]] - digest = "1:e010d6b45ee6c721df761eae89961c634ceb55feff166a48d15504729309f267" - name = "github.com/TheTannerRyan/ring" - packages = ["."] - pruneopts = "" - revision = "7b27005873e31b5d5a035e166636a09e03aaf40e" - version = "v1.1.1" - -[[projects]] - digest = "1:21caed545a1c7ef7a2627bbb45989f689872ff6d5087d49c31340ce74c36de59" - name = "github.com/agext/levenshtein" - packages = ["."] - pruneopts = "" - revision = "52c14c47d03211d8ac1834e94601635e07c5a6ef" - version = "v1.2.3" - -[[projects]] - branch = "v2.1" - digest = "1:3fc5d0d9cb474736e8e6c2f2292e0763b5132c6e7d8cbedf7bde404a470c8c3b" - name = "github.com/cookieo9/resources-go" - packages = ["."] - pruneopts = "" - revision = "d27c04069d0d5dfe11c202dacbf745ae8d1ab181" - -[[projects]] - digest = "1:f384a8b6f89c502229e9013aa4f89ce5b5b56f09f9a4d601d7f1f026d3564fbf" - name = "github.com/coreos/go-iptables" - packages = ["iptables"] - pruneopts = "" - revision = "f901d6c2a4f2a4df092b98c33366dfba1f93d7a0" - version = "v0.4.5" - -[[projects]] - digest = "1:0deddd908b6b4b768cfc272c16ee61e7088a60f7fe2f06c547bd3d8e1f8b8e77" - name = "github.com/davecgh/go-spew" - packages = ["spew"] - pruneopts = "" - revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" - version = "v1.1.1" - -[[projects]] - branch = "master" - digest = "1:c8098f53cd182561cfb128c9a5ba70e41ad2364b763f33f05c6bd54003ae6495" - name = "github.com/florianl/go-nfqueue" - packages = [ - ".", - "internal/unix", - ] - pruneopts = "" - revision = "a2f196e98ab0ffdcb8b5252e7cbba98e45dea204" - -[[projects]] - digest = "1:b6581f9180e0f2d5549280d71819ab951db9d511478c87daca95669589d505c0" - name = "github.com/go-ole/go-ole" - packages = [ - ".", - "oleutil", - ] - pruneopts = "" - revision = "97b6244175ae18ea6eef668034fd6565847501c9" - version = "v1.2.4" - -[[projects]] - digest = "1:f63933986e63230fc32512ed00bc18ea4dbb0f57b5da18561314928fd20c2ff0" - name = "github.com/godbus/dbus" - packages = ["."] - pruneopts = "" - revision = "37bf87eef99d69c4f1d3528bd66e3a87dc201472" - version = "v5.0.3" - -[[projects]] - digest = "1:c18de9c9afca0ab336a29cf356d566abbdc29dd4948547557ed62c0da30d3be3" - name = "github.com/google/gopacket" - packages = [ - ".", - "layers", - "tcpassembly", - ] - pruneopts = "" - revision = "558173e197d46ae52f0f7c58313c96296ee16a9c" - version = "v1.1.18" - -[[projects]] - digest = "1:20dc576ad8f98fe64777c62f090a9b37dd67c62b23fe42b429c2c41936aa8a9c" - name = "github.com/google/renameio" - packages = ["."] - pruneopts = "" - revision = "f0e32980c006571efd537032e5f9cd8c1a92819e" - version = "v0.1.0" - -[[projects]] - digest = "1:8e3bd93036b4a925fe2250d3e4f38f21cadb8ef623561cd80c3c50c114b13201" - name = "github.com/hashicorp/errwrap" - packages = ["."] - pruneopts = "" - revision = "8a6fb523712970c966eefc6b39ed2c5e74880354" - version = "v1.0.0" - -[[projects]] - digest = "1:c6e569ffa34fcd24febd3562bff0520a104d15d1a600199cb3141debf2e58c89" - name = "github.com/hashicorp/go-multierror" - packages = ["."] - pruneopts = "" - revision = "2004d9dba6b07a5b8d133209244f376680f9d472" - version = "v1.1.0" - -[[projects]] - digest = "1:ebffb4b4c8ddcf66bb549464183ea2ddbac6c58a803658f67249f83395d17455" - name = "github.com/hashicorp/go-version" - packages = ["."] - pruneopts = "" - revision = "59da58cfd357de719a4d16dac30481391a56c002" - version = "v1.2.1" - -[[projects]] - digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be" - name = "github.com/inconshreveable/mousetrap" - packages = ["."] - pruneopts = "" - revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" - version = "v1.0" - -[[projects]] - branch = "master" - digest = "1:e71cc6b377264002aec0d9c235087e51ad7a3c1fb341bb4baa84709308b94fe8" - name = "github.com/kardianos/osext" - packages = ["."] - pruneopts = "" - revision = "2bc1f35cddc0cc527b4bc3dce8578fc2a6c11384" - -[[projects]] - digest = "1:711ec17a2d8edd94cff8e2e4339d847e46acc1bb6b49ec29dcc1db78b666378b" - name = "github.com/mdlayher/netlink" - packages = [ - ".", - "nlenc", - ] - pruneopts = "" - revision = "2a4e26491f1ba4eae173a7733ac11744cfed82b5" - version = "v1.2.0" - -[[projects]] - digest = "1:508f444b8e00a569a40899aaf5740348b44c305d36f36d4f002b277677deef95" - name = "github.com/miekg/dns" - packages = ["."] - pruneopts = "" - revision = "10e0aeedbee54849adab780611454192a9980443" - version = "v1.1.33" - -[[projects]] - digest = "1:3282ac9a9ddf5c2c0eda96693364d34fe0f8d10a0748259082a5c9fbd3e1f7e4" - name = "github.com/oschwald/maxminddb-golang" - packages = ["."] - pruneopts = "" - revision = "2e4624cc0c4105b1df1d0643ac3aadb53824dc7d" - version = "v1.7.0" - -[[projects]] - digest = "1:c45802472e0c06928cd997661f2af610accd85217023b1d5f6331bebce0671d3" - name = "github.com/pkg/errors" - packages = ["."] - pruneopts = "" - revision = "614d223910a179a466c1767a985424175c39b465" - version = "v0.9.1" - -[[projects]] - digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411" - name = "github.com/pmezard/go-difflib" - packages = ["difflib"] - pruneopts = "" - revision = "792786c7400a136282c1664665ae0a8db921c6c2" - version = "v1.0.0" - -[[projects]] - digest = "1:70e15b4090e254d1eada6ef156773c0888cf707c43078479114d814761b902c5" - name = "github.com/shirou/gopsutil" - packages = [ - "cpu", - "internal/common", - "mem", - "net", - "process", - ] - pruneopts = "" - revision = "7e94bb8bcde053b6d6c98bda5145e9742c913c39" - version = "v2.20.7" - -[[projects]] - digest = "1:bff75d4f1a2d2c4b8f4b46ff5ac230b80b5fa49276f615900cba09fe4c97e66e" - name = "github.com/spf13/cobra" - packages = ["."] - pruneopts = "" - revision = "a684a6d7f5e37385d954dd3b5a14fc6912c6ab9d" - version = "v1.0.0" - -[[projects]] - digest = "1:688428eeb1ca80d92599eb3254bdf91b51d7e232fead3a73844c1f201a281e51" - name = "github.com/spf13/pflag" - packages = ["."] - pruneopts = "" - revision = "2e9d26c8c37aae03e3f9d4e90b7116f5accb7cab" - version = "v1.0.5" - -[[projects]] - digest = "1:83fd2513b9f6ae0997bf646db6b74e9e00131e31002116fda597175f25add42d" - name = "github.com/stretchr/testify" - packages = ["assert"] - pruneopts = "" - revision = "f654a9112bbeac49ca2cd45bfbe11533c4666cf8" - version = "v1.6.1" - -[[projects]] - digest = "1:1f11a269b089908c141f78c060991ff7bcd16545e95ee48d557e638fa846bde2" - name = "github.com/tevino/abool" - packages = ["."] - pruneopts = "" - revision = "8ae5c93531aabf12924a5b78e6dee1216bfff2f8" - version = "v1.2.0" - -[[projects]] - branch = "master" - digest = "1:21097653bd7914de1262f2429e277933507442f892815a791ce1c0dbf0a8dc20" - name = "github.com/umahmood/haversine" - packages = ["."] - pruneopts = "" - revision = "808ab04add26660fd241ddb7973886c6dd6669e8" - -[[projects]] - branch = "master" - digest = "1:df4642a605244e62c69ae335ac3c3cfa1c2b7ec971c3de398e1909592a961923" - name = "golang.org/x/crypto" - packages = [ - "ed25519", - "ed25519/internal/edwards25519", - ] - pruneopts = "" - revision = "123391ffb6de907695e1066dc40c1ff09322aeb6" - -[[projects]] - digest = "1:ba49944a3238ae8f163c85b6d01d2db51cd5b09807105a3cfaacbd414744ca82" - name = "golang.org/x/mod" - packages = ["semver"] - pruneopts = "" - revision = "859b3ef565e237f9f1a0fb6b55385c497545680d" - version = "v0.3.0" - -[[projects]] - branch = "master" - digest = "1:9ee0e6bc20d85d179d19be321443639dc501a8c0ba1bac173261b57768063e79" - name = "golang.org/x/net" - packages = [ - "bpf", - "icmp", - "idna", - "internal/iana", - "internal/socket", - "ipv4", - "ipv6", - "publicsuffix", - ] - pruneopts = "" - revision = "3edf25e44fccea9e11b919341e952fca722ef460" - -[[projects]] - branch = "master" - digest = "1:ae1578a64c2b241c13ab243739d05936d83825d2b6e9ff043ea3c7105666493d" - name = "golang.org/x/sync" - packages = [ - "errgroup", - "singleflight", - ] - pruneopts = "" - revision = "6e8e738ad208923de99951fe0b48239bfd864f28" - -[[projects]] - branch = "master" - digest = "1:ecfcd51736bf55de713770df4580026a43f01a94c9c077b0ab10239e8a93a589" - name = "golang.org/x/sys" - packages = [ - "internal/unsafeheader", - "unix", - "windows", - "windows/registry", - "windows/svc", - "windows/svc/debug", - "windows/svc/eventlog", - "windows/svc/mgr", - ] - pruneopts = "" - revision = "3ff754bf58a9922e2b8a1a0bd199be6c9a806123" - -[[projects]] - digest = "1:fccda34e4c58111b1908d8d69bf8d57c41c8e2542bc18ec8cd38c4fa21057f71" - name = "golang.org/x/text" - packages = [ - "collate", - "collate/build", - "internal/colltab", - "internal/gen", - "internal/language", - "internal/language/compact", - "internal/tag", - "internal/triegen", - "internal/ucd", - "language", - "secure/bidirule", - "transform", - "unicode/bidi", - "unicode/cldr", - "unicode/norm", - "unicode/rangetable", - ] - pruneopts = "" - revision = "23ae387dee1f90d29a23c0e87ee0b46038fbed0e" - version = "v0.3.3" - -[[projects]] - branch = "master" - digest = "1:1f61b0af124800c576e5ccc355d0634413e0b71fe6fbc77694b18bd30d9aa56e" - name = "golang.org/x/tools" - packages = [ - "go/ast/astutil", - "go/gcexportdata", - "go/internal/gcimporter", - "go/internal/packagesdriver", - "go/packages", - "go/types/typeutil", - "internal/event", - "internal/event/core", - "internal/event/keys", - "internal/event/label", - "internal/gocommand", - "internal/packagesinternal", - "internal/typesinternal", - ] - pruneopts = "" - revision = "d00afeaade8f1e68fb815705aa42d704c1b6df35" - -[[projects]] - branch = "master" - digest = "1:a5a7a1a9560c0eb1f8b32c40da2e71bd2a05b9ff9e1ea294461c7dbe0d24c6bc" - name = "golang.org/x/xerrors" - packages = [ - ".", - "internal", - ] - pruneopts = "" - revision = "5ec99f83aff198f5fbd629d6c8d8eb38a04218ca" - -[[projects]] - branch = "v3" - digest = "1:2e9c4d6def1d36dcd17730e00c06b49a2e97ea5e1e639bcd24fa60fa43e33ad6" - name = "gopkg.in/yaml.v3" - packages = ["."] - pruneopts = "" - revision = "eeeca48fe7764f320e4870d231902bf9c1be2c08" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - input-imports = [ - "github.com/TheTannerRyan/ring", - "github.com/agext/levenshtein", - "github.com/cookieo9/resources-go", - "github.com/coreos/go-iptables/iptables", - "github.com/florianl/go-nfqueue", - "github.com/godbus/dbus", - "github.com/google/gopacket", - "github.com/google/gopacket/layers", - "github.com/google/gopacket/tcpassembly", - "github.com/google/renameio", - "github.com/hashicorp/go-multierror", - "github.com/hashicorp/go-version", - "github.com/miekg/dns", - "github.com/oschwald/maxminddb-golang", - "github.com/shirou/gopsutil/process", - "github.com/spf13/cobra", - "github.com/stretchr/testify/assert", - "github.com/tevino/abool", - "github.com/umahmood/haversine", - "golang.org/x/net/icmp", - "golang.org/x/net/ipv4", - "golang.org/x/net/publicsuffix", - "golang.org/x/sync/errgroup", - "golang.org/x/sync/singleflight", - "golang.org/x/sys/unix", - "golang.org/x/sys/windows", - "golang.org/x/sys/windows/svc", - "golang.org/x/sys/windows/svc/debug", - "golang.org/x/sys/windows/svc/eventlog", - "golang.org/x/sys/windows/svc/mgr", - ] - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml deleted file mode 100644 index 80648950..00000000 --- a/Gopkg.toml +++ /dev/null @@ -1,35 +0,0 @@ -# Gopkg.toml example -# -# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html -# for detailed Gopkg.toml documentation. -# -# required = ["github.com/user/thing/cmd/thing"] -# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] -# -# [[constraint]] -# name = "github.com/user/project" -# version = "1.0.0" -# -# [[constraint]] -# name = "github.com/user/project2" -# branch = "dev" -# source = "github.com/myfork/project2" -# -# [[override]] -# name = "github.com/x/y" -# version = "2.4.0" -# -# [prune] -# non-go = false -# go-tests = true -# unused-packages = true - -ignored = ["github.com/safing/portbase/*", "github.com/safing/spn/*"] - -[[constraint]] - name = "github.com/florianl/go-nfqueue" - branch = "master" # switch back once we migrate to go.mod - -[[override]] - name = "github.com/mdlayher/netlink" - version = "1.2.0" # remove when github.com/florianl/go-nfqueue has updated to v1.2.0 diff --git a/assets/.gitkeep b/assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/cmds/hub/.gitignore b/cmds/hub/.gitignore new file mode 100644 index 00000000..41668e89 --- /dev/null +++ b/cmds/hub/.gitignore @@ -0,0 +1,3 @@ +# Compiled binaries +hub +hub.exe diff --git a/cmds/hub/build b/cmds/hub/build new file mode 100755 index 00000000..055874ef --- /dev/null +++ b/cmds/hub/build @@ -0,0 +1,60 @@ +#!/bin/bash + +# get build data +if [[ "$BUILD_COMMIT" == "" ]]; then + BUILD_COMMIT=$(git describe --all --long --abbrev=99 --dirty 2>/dev/null) +fi +if [[ "$BUILD_USER" == "" ]]; then + BUILD_USER=$(id -un) +fi +if [[ "$BUILD_HOST" == "" ]]; then + BUILD_HOST=$(hostname -f) +fi +if [[ "$BUILD_DATE" == "" ]]; then + BUILD_DATE=$(date +%d.%m.%Y) +fi +if [[ "$BUILD_SOURCE" == "" ]]; then + BUILD_SOURCE=$(git remote -v | grep origin | cut -f2 | cut -d" " -f1 | head -n 1) +fi +if [[ "$BUILD_SOURCE" == "" ]]; then + BUILD_SOURCE=$(git remote -v | cut -f2 | cut -d" " -f1 | head -n 1) +fi +BUILD_BUILDOPTIONS=$(echo $* | sed "s/ /§/g") + +# check +if [[ "$BUILD_COMMIT" == "" ]]; then + echo "could not automatically determine BUILD_COMMIT, please supply manually as environment variable." + exit 1 +fi +if [[ "$BUILD_USER" == "" ]]; then + echo "could not automatically determine BUILD_USER, please supply manually as environment variable." + exit 1 +fi +if [[ "$BUILD_HOST" == "" ]]; then + echo "could not automatically determine BUILD_HOST, please supply manually as environment variable." + exit 1 +fi +if [[ "$BUILD_DATE" == "" ]]; then + echo "could not automatically determine BUILD_DATE, please supply manually as environment variable." + exit 1 +fi +if [[ "$BUILD_SOURCE" == "" ]]; then + echo "could not automatically determine BUILD_SOURCE, please supply manually as environment variable." + exit 1 +fi + +# set build options +export CGO_ENABLED=0 +if [[ $1 == "dev" ]]; then + shift + export CGO_ENABLED=1 + DEV="-race" +fi + +echo "Please notice, that this build script includes metadata into the build." +echo "This information is useful for debugging and license compliance." +echo "Run the compiled binary with the -version flag to see the information included." + +# build +BUILD_PATH="github.com/safing/portbase/info" +go build $DEV -ldflags "-X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" $* diff --git a/cmds/hub/main.go b/cmds/hub/main.go new file mode 100644 index 00000000..831e3abc --- /dev/null +++ b/cmds/hub/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "flag" + "fmt" + "os" + "runtime" + + "github.com/safing/portbase/info" + "github.com/safing/portbase/metrics" + "github.com/safing/portbase/modules" + "github.com/safing/portbase/run" + _ "github.com/safing/portmaster/service/core/base" + _ "github.com/safing/portmaster/service/ui" + "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/service/updates/helper" + _ "github.com/safing/portmaster/spn/captain" + "github.com/safing/portmaster/spn/conf" +) + +func init() { + flag.BoolVar(&updates.RebootOnRestart, "reboot-on-restart", false, "reboot server on auto-upgrade") +} + +func main() { + info.Set("SPN Hub", "0.7.6", "AGPLv3", true) + + // Configure metrics. + _ = metrics.SetNamespace("hub") + + // Configure updating. + updates.UserAgent = fmt.Sprintf("SPN Hub (%s %s)", runtime.GOOS, runtime.GOARCH) + helper.IntelOnly() + + // Configure SPN mode. + conf.EnablePublicHub(true) + conf.EnableClient(false) + + // Disable module management, as we want to start all modules. + modules.DisableModuleManagement() + + // Configure microtask threshold. + // Scale with CPU/GOMAXPROCS count, but keep a baseline and minimum: + // CPUs -> MicroTasks + // 0 -> 8 (increased to minimum) + // 1 -> 8 (increased to minimum) + // 2 -> 8 + // 3 -> 10 + // 4 -> 12 + // 8 -> 20 + // 16 -> 36 + // + // Start with number of GOMAXPROCS. + microTasksThreshold := runtime.GOMAXPROCS(0) * 2 + // Use at least 4 microtasks based on GOMAXPROCS. + if microTasksThreshold < 4 { + microTasksThreshold = 4 + } + // Add a 4 microtask baseline. + microTasksThreshold += 4 + // Set threshold. + modules.SetMaxConcurrentMicroTasks(microTasksThreshold) + + // Start. + os.Exit(run.Run()) +} diff --git a/cmds/hub/pack b/cmds/hub/pack new file mode 100755 index 00000000..73c20270 --- /dev/null +++ b/cmds/hub/pack @@ -0,0 +1,123 @@ +#!/bin/bash + +baseDir="$( cd "$(dirname "$0")" && pwd )" +cd "$baseDir" + +COL_OFF="\033[0m" +COL_BOLD="\033[01;01m" +COL_RED="\033[31m" +COL_GREEN="\033[32m" +COL_YELLOW="\033[33m" + +destDirPart1="../../dist" +destDirPart2="hub" + +function prep { + # output + output="main" + # get version + version=$(grep "info.Set" main.go | cut -d'"' -f4) + # build versioned file name + filename="spn-hub_v${version//./-}" + # platform + platform="${GOOS}_${GOARCH}" + if [[ $GOOS == "windows" ]]; then + filename="${filename}.exe" + output="${output}.exe" + fi + # build destination path + destPath=${destDirPart1}/${platform}/${destDirPart2}/$filename +} + +function check { + prep + + # check if file exists + if [[ -f $destPath ]]; then + echo "[hub] $platform v$version already built" + else + echo -e "${COL_BOLD}[hub] $platform v$version${COL_OFF}" + fi +} + +function build { + prep + + # check if file exists + if [[ -f $destPath ]]; then + echo "[hub] $platform already built in v$version, skipping..." + return + fi + + # build + ./build main.go + if [[ $? -ne 0 ]]; then + echo -e "\n${COL_BOLD}[hub] $platform v$version: ${COL_RED}BUILD FAILED.${COL_OFF}" + exit 1 + fi + mkdir -p $(dirname $destPath) + cp $output $destPath + echo -e "\n${COL_BOLD}[hub] $platform v$version: ${COL_GREEN}successfully built.${COL_OFF}" +} + +function reset { + prep + + # delete if file exists + if [[ -f $destPath ]]; then + rm $destPath + echo "[hub] $platform v$version deleted." + fi +} + +function check_all { + GOOS=linux GOARCH=amd64 check + GOOS=windows GOARCH=amd64 check + GOOS=darwin GOARCH=amd64 check + GOOS=linux GOARCH=arm64 check + GOOS=windows GOARCH=arm64 check + GOOS=darwin GOARCH=arm64 check +} + +function build_all { + GOOS=linux GOARCH=amd64 build + GOOS=windows GOARCH=amd64 build + GOOS=darwin GOARCH=amd64 build + GOOS=linux GOARCH=arm64 build + GOOS=windows GOARCH=arm64 build + GOOS=darwin GOARCH=arm64 build +} + +function reset_all { + GOOS=linux GOARCH=amd64 reset + GOOS=windows GOARCH=amd64 reset + GOOS=darwin GOARCH=amd64 reset + GOOS=linux GOARCH=arm64 reset + GOOS=windows GOARCH=arm64 reset + GOOS=darwin GOARCH=arm64 reset +} + +case $1 in + "check" ) + check_all + ;; + "build" ) + build_all + ;; + "reset" ) + reset_all + ;; + * ) + echo "" + echo "build list:" + echo "" + check_all + echo "" + read -p "press [Enter] to start building" x + echo "" + build_all + echo "" + echo "finished building." + echo "" + ;; +esac diff --git a/cmds/integrationtest/netstate.go b/cmds/integrationtest/netstate.go index f76604c7..5eaaa9c8 100644 --- a/cmds/integrationtest/netstate.go +++ b/cmds/integrationtest/netstate.go @@ -7,9 +7,9 @@ import ( processInfo "github.com/shirou/gopsutil/process" "github.com/spf13/cobra" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/network/socket" - "github.com/safing/portmaster/network/state" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/network/socket" + "github.com/safing/portmaster/service/network/state" ) func init() { diff --git a/cmds/observation-hub/.gitignore b/cmds/observation-hub/.gitignore new file mode 100644 index 00000000..f1b57325 --- /dev/null +++ b/cmds/observation-hub/.gitignore @@ -0,0 +1,3 @@ +# Compiled binaries +observation-hub +observation-hub.exe diff --git a/cmds/observation-hub/Dockerfile b/cmds/observation-hub/Dockerfile new file mode 100644 index 00000000..0ff78cb9 --- /dev/null +++ b/cmds/observation-hub/Dockerfile @@ -0,0 +1,38 @@ +# Docker Image for Observation Hub + +# Important: +# You need to build this from the repo root! +# Run: docker build -f cmds/observation-hub/Dockerfile -t safing/observation-hub:latest . +# Check With: docker run -ti --rm safing/observation-hub:latest --help + +# golang 1.21 linux/amd64 on debian bookworm +# https://github.com/docker-library/golang/blob/master/1.21/bookworm/Dockerfile +FROM golang:1.21-bookworm as builder + +# Ensure ca-certficates are up to date +RUN update-ca-certificates + +# Install dependencies +WORKDIR $GOPATH/src/github.com/safing/portmaster/spn +COPY go.mod . +COPY go.sum . +ENV GO111MODULE=on +RUN go mod download +RUN go mod verify + +# Copy source code +COPY . . + +# Build the static binary +RUN cd cmds/observation-hub && \ +CGO_ENABLED=0 ./build -o /go/bin/observation-hub + +# Use static image +# https://github.com/GoogleContainerTools/distroless +FROM gcr.io/distroless/static-debian12 + +# Copy our static executable +COPY --from=builder --chmod=0755 /go/bin/observation-hub /go/bin/observation-hub + +# Run the observation-hub binary. +ENTRYPOINT ["/go/bin/observation-hub"] diff --git a/cmds/observation-hub/apprise.go b/cmds/observation-hub/apprise.go new file mode 100644 index 00000000..c7df3c19 --- /dev/null +++ b/cmds/observation-hub/apprise.go @@ -0,0 +1,257 @@ +package main + +import ( + "bytes" + "crypto/tls" + _ "embed" + "errors" + "flag" + "fmt" + "net/http" + "strings" + "text/template" + "time" + + "github.com/safing/portbase/apprise" + "github.com/safing/portbase/log" + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/service/intel/geoip" +) + +var ( + appriseModule *modules.Module + appriseNotifier *apprise.Notifier + + appriseURL string + appriseTag string + appriseClientCert string + appriseClientKey string + appriseGreet bool +) + +func init() { + appriseModule = modules.Register("apprise", nil, startApprise, nil) + + flag.StringVar(&appriseURL, "apprise-url", "", "set the apprise URL to enable notifications via apprise") + flag.StringVar(&appriseTag, "apprise-tag", "", "set the apprise tag(s) according to their docs") + flag.StringVar(&appriseClientCert, "apprise-client-cert", "", "set the apprise client certificate") + flag.StringVar(&appriseClientKey, "apprise-client-key", "", "set the apprise client key") + flag.BoolVar(&appriseGreet, "apprise-greet", false, "send a greeting message to apprise on start") +} + +func startApprise() error { + // Check if apprise should be configured. + if appriseURL == "" { + return nil + } + // Check if there is a tag. + if appriseTag == "" { + return errors.New("an apprise tag is required") + } + + // Create notifier. + appriseNotifier = &apprise.Notifier{ + URL: appriseURL, + DefaultType: apprise.TypeInfo, + DefaultTag: appriseTag, + DefaultFormat: apprise.FormatMarkdown, + AllowUntagged: false, + } + + if appriseClientCert != "" || appriseClientKey != "" { + // Load client cert from disk. + cert, err := tls.LoadX509KeyPair(appriseClientCert, appriseClientKey) + if err != nil { + return fmt.Errorf("failed to load client cert/key: %w", err) + } + + // Set client cert in http client. + appriseNotifier.SetClient(&http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + Certificates: []tls.Certificate{cert}, + }, + }, + Timeout: 10 * time.Second, + }) + } + + if appriseGreet { + err := appriseNotifier.Send(appriseModule.Ctx, &apprise.Message{ + Title: "👋 Observation Hub Reporting In", + Body: "I am the Observation Hub. I am connected to the SPN and watch out for it. I will report notable changes to the network here.", + }) + if err != nil { + log.Warningf("apprise: failed to send test message: %s", err) + } else { + log.Info("apprise: sent greeting message") + } + } + + return nil +} + +func reportToApprise(change *observedChange) (errs error) { + // Check if configured. + if appriseNotifier == nil { + return nil + } + +handleTag: + for _, tag := range strings.Split(appriseNotifier.DefaultTag, ",") { + // Check if we are shutting down. + if appriseModule.IsStopping() { + return nil + } + + // Render notification based on tag / destination. + buf := &bytes.Buffer{} + switch { + case strings.HasPrefix(tag, "matrix-"): + if err := templates.ExecuteTemplate(buf, "matrix-notification", change); err != nil { + return fmt.Errorf("failed to render notification: %w", err) + } + + case strings.HasPrefix(tag, "discord-"): + if err := templates.ExecuteTemplate(buf, "discord-notification", change); err != nil { + return fmt.Errorf("failed to render notification: %w", err) + } + + default: + // Use matrix notification template as default for now. + if err := templates.ExecuteTemplate(buf, "matrix-notification", change); err != nil { + return fmt.Errorf("failed to render notification: %w", err) + } + } + + // Send notification to apprise. + var err error + for i := 0; i < 3; i++ { + // Try three times. + err = appriseNotifier.Send(appriseModule.Ctx, &apprise.Message{ + Body: buf.String(), + Tag: tag, + }) + if err == nil { + continue handleTag + } + // Wait for 5 seconds, then try again. + time.Sleep(5 * time.Second) + } + // Add error to errors. + if err != nil { + errs = errors.Join(errs, fmt.Errorf("| failed to send: %w", err)) + } + } + + return errs +} + +// var ( +// entityTemplate = template.Must(template.New("entity").Parse( +// `Entity: {{ . }} +// {{ .IP }} [{{ .ASN }} - {{ .ASOrg }}] +// `, +// )) + +// // {{ with .GetCountryInfo -}} +// // {{ .Name }} ({{ .Code }}) +// // {{- end }} + +// matrixTemplate = template.Must(template.New("matrix observer notification").Parse( +// `{{ .Title }} +// {{ if .Summary }} +// Details: +// {{ .Summary }} + +// Note: Changes were registered at {{ .UpdateTime }} and were possibly merged. +// {{ end }} + +// {{ template "entity" .UpdatedPin.EntityV4 }} + +// Hub Info: +// Test: {{ .UpdatedPin.EntityV4 }} +// {{ template "entity" .UpdatedPin.EntityV4 }} +// {{ template "entity" .UpdatedPin.EntityV6 }} +// `, +// )) + +// discordTemplate = template.Must(template.New("discord observer notification").Parse( +// ``, +// )) + +// defaultTemplate = template.Must(template.New("default observer notification").Parse( +// ``, +// )) +// ) + +var ( + //go:embed notifications.tmpl + templateFile string + templates = template.Must(template.New("notifications").Funcs( + template.FuncMap{ + "joinStrings": joinStrings, + "textBlock": textBlock, + "getCountryInfo": getCountryInfo, + }, + ).Parse(templateFile)) +) + +func joinStrings(slice []string, sep string) string { + return strings.Join(slice, sep) +} + +func textBlock(block, addPrefix, addSuffix string) string { + // Trim whitespaces. + block = strings.TrimSpace(block) + + // Prepend and append string for every line. + lines := strings.Split(block, "\n") + for i, line := range lines { + lines[i] = addPrefix + line + addSuffix + } + + // Return as block. + return strings.Join(lines, "\n") +} + +func getCountryInfo(code string) geoip.CountryInfo { + // Get the country info directly instead of via the entity location, + // so it also works in test without the geoip module. + return geoip.GetCountryInfo(code) +} + +// func init() { +// templates = template.Must(template.New(templateFile).Parse(templateFile)) + +// nt, err := templates.New("entity").Parse( +// `Entity: {{ . }} +// {{ .IP }} [{{ .ASN }} - {{ .ASOrg }}] +// `, +// ) +// if err != nil { +// panic(err) +// } +// templates.AddParseTree(nt.Tree) + +// if _, err := templates.New("matrix-notification").Parse( +// `{{ .Title }} +// {{ if .Summary }} +// Details: +// {{ .Summary }} + +// Note: Changes were registered at {{ .UpdateTime }} and were possibly merged. +// {{ end }} + +// {{ template "entity" .UpdatedPin.EntityV4 }} + +// Hub Info: +// Test: {{ .UpdatedPin.EntityV4 }} +// {{ template "entity" .UpdatedPin.EntityV4 }} +// {{ template "entity" .UpdatedPin.EntityV6 }} +// `, +// ); err != nil { +// panic(err) +// } +// } diff --git a/cmds/observation-hub/apprise_test.go b/cmds/observation-hub/apprise_test.go new file mode 100644 index 00000000..e0397858 --- /dev/null +++ b/cmds/observation-hub/apprise_test.go @@ -0,0 +1,84 @@ +package main + +import ( + "bytes" + "fmt" + "net" + "testing" + "time" + + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/navigator" +) + +var observedTestChange = &observedChange{ + Title: "Hub Changed: fogos (8uLe-zUkC)", + Summary: `ConnectedTo.ZwqBAzGqifBAFKFW1GQijNM18pi7BnWH34GyKBF7KB5fC5.HubID removed ZwqBAzGqifBAFKFW1GQijNM18pi7BnWH34GyKBF7KB5fC5 + ConnectedTo.ZwqBAzGqifBAFKFW1GQijNM18pi7BnWH34GyKBF7KB5fC5.Capacity removed 3403661 + ConnectedTo.ZwqBAzGqifBAFKFW1GQijNM18pi7BnWH34GyKBF7KB5fC5.Latency removed 252.350006ms`, + UpdatedPin: &navigator.PinExport{ + ID: "Zwtb8EKMatnMRkW1VaLh8CPV3QswD9iuRU4Sda8uLezUkC", + Name: "fogos", + Map: "main", + FirstSeen: time.Now(), + EntityV4: &intel.Entity{ + IP: net.IPv4(138, 201, 140, 70), + IPScope: netutils.Global, + Country: "DE", + ASN: 24940, + ASOrg: "Hetzner Online GmbH", + }, + States: []string{"HasRequiredInfo", "Reachable", "Active", "Trusted"}, + VerifiedOwner: "Safing", + HopDistance: 3, + SessionActive: false, + Info: &hub.Announcement{ + ID: "Zwtb8EKMatnMRkW1VaLh8CPV3QswD9iuRU4Sda8uLezUkC", + Timestamp: 1677682008, + Name: "fogos", + Group: "Safing", + ContactAddress: "abuse@safing.io", + ContactService: "email", + Hosters: []string{"Hetzner"}, + Datacenter: "DE-Hetzner-FSN", + IPv4: net.IPv4(138, 201, 140, 70), + IPv6: net.ParseIP("2a01:4f8:172:3753::2"), + Transports: []string{"tcp:17", "tcp:17017"}, + Entry: []string{}, + Exit: []string{"- * TCP/25"}, + }, + Status: &hub.Status{ + Timestamp: 1694180778, + Version: "0.6.19 ", + }, + }, + UpdateTime: time.Now(), +} + +func TestNotificationTemplate(t *testing.T) { + t.Parallel() + + fmt.Println("==========\nFound templates:") + for _, tpl := range templates.Templates() { + fmt.Println(tpl.Name()) + } + fmt.Println("") + + fmt.Println("\n\n==========\nMatrix template:") + matrixOutput := &bytes.Buffer{} + err := templates.ExecuteTemplate(matrixOutput, "matrix-notification", observedTestChange) + if err != nil { + t.Errorf("failed to render matrix template: %s", err) + } + fmt.Println(matrixOutput.String()) + + fmt.Println("\n\n==========\nDiscord template:") + discordOutput := &bytes.Buffer{} + err = templates.ExecuteTemplate(discordOutput, "discord-notification", observedTestChange) + if err != nil { + t.Errorf("failed to render discord template: %s", err) + } + fmt.Println(discordOutput.String()) +} diff --git a/cmds/observation-hub/build b/cmds/observation-hub/build new file mode 100755 index 00000000..055874ef --- /dev/null +++ b/cmds/observation-hub/build @@ -0,0 +1,60 @@ +#!/bin/bash + +# get build data +if [[ "$BUILD_COMMIT" == "" ]]; then + BUILD_COMMIT=$(git describe --all --long --abbrev=99 --dirty 2>/dev/null) +fi +if [[ "$BUILD_USER" == "" ]]; then + BUILD_USER=$(id -un) +fi +if [[ "$BUILD_HOST" == "" ]]; then + BUILD_HOST=$(hostname -f) +fi +if [[ "$BUILD_DATE" == "" ]]; then + BUILD_DATE=$(date +%d.%m.%Y) +fi +if [[ "$BUILD_SOURCE" == "" ]]; then + BUILD_SOURCE=$(git remote -v | grep origin | cut -f2 | cut -d" " -f1 | head -n 1) +fi +if [[ "$BUILD_SOURCE" == "" ]]; then + BUILD_SOURCE=$(git remote -v | cut -f2 | cut -d" " -f1 | head -n 1) +fi +BUILD_BUILDOPTIONS=$(echo $* | sed "s/ /§/g") + +# check +if [[ "$BUILD_COMMIT" == "" ]]; then + echo "could not automatically determine BUILD_COMMIT, please supply manually as environment variable." + exit 1 +fi +if [[ "$BUILD_USER" == "" ]]; then + echo "could not automatically determine BUILD_USER, please supply manually as environment variable." + exit 1 +fi +if [[ "$BUILD_HOST" == "" ]]; then + echo "could not automatically determine BUILD_HOST, please supply manually as environment variable." + exit 1 +fi +if [[ "$BUILD_DATE" == "" ]]; then + echo "could not automatically determine BUILD_DATE, please supply manually as environment variable." + exit 1 +fi +if [[ "$BUILD_SOURCE" == "" ]]; then + echo "could not automatically determine BUILD_SOURCE, please supply manually as environment variable." + exit 1 +fi + +# set build options +export CGO_ENABLED=0 +if [[ $1 == "dev" ]]; then + shift + export CGO_ENABLED=1 + DEV="-race" +fi + +echo "Please notice, that this build script includes metadata into the build." +echo "This information is useful for debugging and license compliance." +echo "Run the compiled binary with the -version flag to see the information included." + +# build +BUILD_PATH="github.com/safing/portbase/info" +go build $DEV -ldflags "-X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" $* diff --git a/cmds/observation-hub/main.go b/cmds/observation-hub/main.go new file mode 100644 index 00000000..c82f3a17 --- /dev/null +++ b/cmds/observation-hub/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "os" + "runtime" + + "github.com/safing/portbase/api" + "github.com/safing/portbase/info" + "github.com/safing/portbase/metrics" + "github.com/safing/portbase/modules" + "github.com/safing/portbase/run" + "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/service/updates/helper" + "github.com/safing/portmaster/spn/captain" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/sluice" +) + +func main() { + info.Set("SPN Observation Hub", "0.7.1", "AGPLv3", true) + + // Configure metrics. + _ = metrics.SetNamespace("observer") + + // Configure user agent. + updates.UserAgent = fmt.Sprintf("SPN Observation Hub (%s %s)", runtime.GOOS, runtime.GOARCH) + helper.IntelOnly() + + // Configure SPN mode. + conf.EnableClient(true) + conf.EnablePublicHub(false) + captain.DisableAccount = true + + // Disable unneeded listeners. + sluice.EnableListener = false + api.EnableServer = false + + // Disable module management, as we want to start all modules. + modules.DisableModuleManagement() + + // Start. + os.Exit(run.Run()) +} diff --git a/cmds/observation-hub/notifications.tmpl b/cmds/observation-hub/notifications.tmpl new file mode 100644 index 00000000..8a25175f --- /dev/null +++ b/cmds/observation-hub/notifications.tmpl @@ -0,0 +1,75 @@ +{{ define "entity" -}} + {{ .IP }} [AS{{ .ASN }} - {{ .ASOrg }}] in {{ if .Country }} + {{- with getCountryInfo .Country -}} + {{ .Name }} ({{ .Code }}; Region {{ .Continent.Region }}) + {{- end }} + {{- end }} +{{- end }} + +{{ define "matrix-notification" -}} +### 🌍 {{ .Title }}{{ if .Summary }} + +{{ textBlock .Summary "" " " }} +{{ end }} + +> Note: Changes were registered at {{ .UpdateTime.UTC.Format "15:04:05 02.01.2006 MST" }} and were possibly merged. + +##### Hub Info + +> Name: {{ .UpdatedPin.Name }} +> ID: {{ .UpdatedPin.ID }} +> IPv4: {{ if .UpdatedPin.EntityV4 }}{{ template "entity" .UpdatedPin.EntityV4 }}{{ end }} +> IPv6: {{ if .UpdatedPin.EntityV6 }}{{ template "entity" .UpdatedPin.EntityV6 }}{{ end }} +> Version: {{ .UpdatedPin.Status.Version }} +> States: {{ joinStrings .UpdatedPin.States ", " }} +> Status: {{ len .UpdatedPin.Status.Lanes }} Lanes, {{ len .UpdatedPin.Status.Keys }} Keys, {{ .UpdatedPin.Status.Load }} Load +> Verified Owner: {{ .UpdatedPin.VerifiedOwner }} +> Transports: {{ joinStrings .UpdatedPin.Info.Transports ", " }} +> Entry: {{ joinStrings .UpdatedPin.Info.Entry ", " }} +> Exit: {{ joinStrings .UpdatedPin.Info.Exit ", " }} +> Relations: {{ if .UpdatedPin.Info.Group -}} +Group={{ .UpdatedPin.Info.Group }} {{ end }} + +{{- if .UpdatedPin.Info.Datacenter -}} +Datacenter={{ .UpdatedPin.Info.Datacenter }} {{ end }} + +{{- if .UpdatedPin.Info.Hosters -}} +Hosters={{ joinStrings .UpdatedPin.Info.Hosters ";" }} {{ end }} + +{{- if .UpdatedPin.Info.ContactAddress -}} +Contact= {{ .UpdatedPin.Info.ContactAddress }}{{ if .UpdatedPin.Info.ContactService }} via {{ .UpdatedPin.Info.ContactService }}{{ end }}{{ end }} + +{{- end }} + +{{ define "discord-notification" -}} +# 🌍 {{ .Title }}{{ if .Summary }} + +{{ .Summary }} +{{- end }} + +##### Note: Changes were registered at {{ .UpdateTime.UTC.Format "15:04:05 02.01.2006 MST" }} and were possibly merged. - Hub Info: + +Name: {{ .UpdatedPin.Name }} +ID: {{ .UpdatedPin.ID }} +IPv4: {{ if .UpdatedPin.EntityV4 }}{{ template "entity" .UpdatedPin.EntityV4 }}{{ end }} +IPv6: {{ if .UpdatedPin.EntityV6 }}{{ template "entity" .UpdatedPin.EntityV6 }}{{ end }} +Version: {{ .UpdatedPin.Status.Version }} +States: {{ joinStrings .UpdatedPin.States ", " }} +Status: {{ len .UpdatedPin.Status.Lanes }} Lanes, {{ len .UpdatedPin.Status.Keys }} Keys, {{ .UpdatedPin.Status.Load }} Load +Verified Owner: {{ .UpdatedPin.VerifiedOwner }} +Transports: {{ joinStrings .UpdatedPin.Info.Transports ", " }} +Entry: {{ joinStrings .UpdatedPin.Info.Entry ", " }} +Exit: {{ joinStrings .UpdatedPin.Info.Exit ", " }} +Relations: {{ if .UpdatedPin.Info.Group -}} +Group={{ .UpdatedPin.Info.Group }} {{ end }} + +{{- if .UpdatedPin.Info.Datacenter -}} +Datacenter={{ .UpdatedPin.Info.Datacenter }} {{ end }} + +{{- if .UpdatedPin.Info.Hosters -}} +Hosters={{ joinStrings .UpdatedPin.Info.Hosters ";" }} {{ end }} + +{{- if .UpdatedPin.Info.ContactAddress -}} +Contact= {{ .UpdatedPin.Info.ContactAddress }}{{ if .UpdatedPin.Info.ContactService }} via {{ .UpdatedPin.Info.ContactService }}{{ end }}{{ end }} + +{{- end }} diff --git a/cmds/observation-hub/observe.go b/cmds/observation-hub/observe.go new file mode 100644 index 00000000..371b8692 --- /dev/null +++ b/cmds/observation-hub/observe.go @@ -0,0 +1,407 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "path" + "strings" + "time" + + diff "github.com/r3labs/diff/v3" + "golang.org/x/exp/slices" + + "github.com/safing/portbase/database" + "github.com/safing/portbase/database/query" + "github.com/safing/portbase/log" + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/spn/captain" + "github.com/safing/portmaster/spn/navigator" +) + +var ( + observerModule *modules.Module + + db = database.NewInterface(&database.Options{ + Local: true, + Internal: true, + }) + + reportAllChanges bool + + errNoChanges = errors.New("no changes") + + reportingDelayFlag string + reportingDelay = 10 * time.Minute +) + +func init() { + observerModule = modules.Register("observer", prepObserver, startObserver, nil, "captain", "apprise") + + flag.BoolVar(&reportAllChanges, "report-all-changes", false, "report all changes, no just interesting ones") + flag.StringVar(&reportingDelayFlag, "reporting-delay", "10m", "delay reports to summarize changes") +} + +func prepObserver() error { + if reportingDelayFlag != "" { + duration, err := time.ParseDuration(reportingDelayFlag) + if err != nil { + return fmt.Errorf("failed to parse reporting-delay: %w", err) + } + reportingDelay = duration + } + + return nil +} + +func startObserver() error { + observerModule.StartServiceWorker("observer", 0, observerWorker) + + return nil +} + +type observedPin struct { + previous *navigator.PinExport + latest *navigator.PinExport + + lastUpdate time.Time + lastUpdateReported bool +} + +type observedChange struct { + Title string + Summary string + + UpdatedPin *navigator.PinExport + UpdateTime time.Time + + SPNStatus *captain.SPNStatus +} + +func observerWorker(ctx context.Context) error { + log.Info("observer: starting") + defer log.Info("observer: stopped") + + // Subscribe to SPN status. + statusSub, err := db.Subscribe(query.New("runtime:spn/status")) + if err != nil { + return fmt.Errorf("failed to subscribe to spn status: %w", err) + } + defer statusSub.Cancel() //nolint:errcheck + + // Get latest status. + latestStatus := captain.GetSPNStatus() + + // Step 1: Wait for SPN to connect, if needed. + if latestStatus.Status != captain.StatusConnected { + log.Info("observer: waiting for SPN to connect") + waitForConnect: + for { + select { + case r := <-statusSub.Feed: + if r == nil { + return errors.New("status feed ended") + } + + statusUpdate, ok := r.(*captain.SPNStatus) + switch { + case !ok: + log.Warningf("observer: received invalid SPN status: %s", r) + case statusUpdate.Status == captain.StatusFailed: + log.Warningf("observer: SPN failed to connect") + case statusUpdate.Status == captain.StatusConnected: + break waitForConnect + } + case <-ctx.Done(): + return nil + } + } + } + + // Wait for one second for the navigator to settle things. + log.Info("observer: connected to network, waiting for navigator") + time.Sleep(1 * time.Second) + + // Step 2: Get current state. + mapQuery := query.New("map:main/") + q, err := db.Query(mapQuery) + if err != nil { + return fmt.Errorf("failed to start map query: %w", err) + } + defer q.Cancel() + + // Put all current pins in a map. + observedPins := make(map[string]*observedPin) +query: + for { + select { + case r := <-q.Next: + // Check if we are done. + if r == nil { + break query + } + // Add all pins to seen pins. + if pin, ok := r.(*navigator.PinExport); ok { + observedPins[pin.ID] = &observedPin{ + previous: pin, + latest: pin, + } + } else { + log.Warningf("observer: received invalid pin export: %s", r) + } + case <-ctx.Done(): + return nil + } + } + if q.Err() != nil { + return fmt.Errorf("failed to finish map query: %w", q.Err()) + } + + // Step 3: Monitor for changes. + sub, err := db.Subscribe(mapQuery) + if err != nil { + return fmt.Errorf("failed to start map sub: %w", err) + } + defer sub.Cancel() //nolint:errcheck + + // Start ticker for checking for changes. + reportChangesTicker := time.NewTicker(10 * time.Second) + defer reportChangesTicker.Stop() + + log.Info("observer: listening for hub changes") + for { + select { + case <-ctx.Done(): + return nil + + case r := <-statusSub.Feed: + // Keep SPN connection status up to date. + if r == nil { + return errors.New("status feed ended") + } + if statusUpdate, ok := r.(*captain.SPNStatus); ok { + latestStatus = statusUpdate + log.Infof("observer: SPN status is now %s", statusUpdate.Status) + } else { + log.Warningf("observer: received invalid pin export: %s", r) + } + + case r := <-sub.Feed: + // Save all observed pins. + switch { + case r == nil: + return errors.New("pin feed ended") + case r.Meta().IsDeleted(): + delete(observedPins, path.Base(r.DatabaseKey())) + default: + if pin, ok := r.(*navigator.PinExport); ok { + existingObservedPin, ok := observedPins[pin.ID] + if ok { + // Update previously observed Hub. + existingObservedPin.latest = pin + existingObservedPin.lastUpdate = time.Now() + existingObservedPin.lastUpdateReported = false + } else { + // Add new Hub. + observedPins[pin.ID] = &observedPin{ + latest: pin, + lastUpdate: time.Now(), + } + } + } else { + log.Warningf("observer: received invalid pin export: %s", r) + } + } + + case <-reportChangesTicker.C: + // Report changed pins. + + for _, observedPin := range observedPins { + // Check if context was canceled. + select { + case <-ctx.Done(): + return nil + default: + } + + switch { + case observedPin.lastUpdateReported: + // Change already reported. + case time.Since(observedPin.lastUpdate) < reportingDelay: + // Only report changes if older than the configured delay. + default: + // Format and report. + title, changes, err := formatPinChanges(observedPin.previous, observedPin.latest) + if err != nil { + if !errors.Is(err, errNoChanges) { + log.Warningf("observer: failed to format pin changes: %s", err) + } + } else { + // Report changes. + reportChanges(&observedChange{ + Title: title, + Summary: changes, + UpdatedPin: observedPin.latest, + UpdateTime: observedPin.lastUpdate, + SPNStatus: latestStatus, + }) + } + + // Update observed pin. + observedPin.previous = observedPin.latest + observedPin.lastUpdateReported = true + } + } + } + } +} + +func reportChanges(change *observedChange) { + // Log changes. + log.Infof("observer:\n%s\n%s", change.Title, change.Summary) + + // Report via Apprise. + err := reportToApprise(change) + if err != nil { + log.Warningf("observer: failed to report changes to apprise: %s", err) + } +} + +var ( + ignoreChangesIn = []string{ + "ConnectedTo", + "HopDistance", + "Info.entryPolicy", // Alternatively, ignore "Info.Entry" + "Info.exitPolicy", // Alternatively, ignore "Info.Exit" + "Info.parsedTransports", + "Info.Timestamp", + "SessionActive", + "Status.Keys", + "Status.Lanes", + "Status.Load", + "Status.Timestamp", + } + + ignoreStates = []string{ + "IsHomeHub", + "Failing", + } +) + +func ignoreChange(path string) bool { + if reportAllChanges { + return false + } + + for _, pathPrefix := range ignoreChangesIn { + if strings.HasPrefix(path, pathPrefix) { + return true + } + } + return false +} + +func formatPinChanges(from, to *navigator.PinExport) (title, changes string, err error) { + // Return immediately if pin is new. + if from == nil { + return fmt.Sprintf("New Hub: %s", makeHubName(to.Name, to.ID)), "", nil + } + + // Find notable changes. + changelog, err := diff.Diff(from, to) + if err != nil { + return "", "", fmt.Errorf("failed to diff: %w", err) + } + if len(changelog) > 0 { + // Build changelog message. + changes := make([]string, 0, len(changelog)) + for _, change := range changelog { + // Create path to changed field. + fullPath := strings.Join(change.Path, ".") + + // Check if this path should be ignored. + if ignoreChange(fullPath) { + continue + } + + // Add to reportable changes. + changeMsg := formatChange(change, fullPath) + if changeMsg != "" { + changes = append(changes, changeMsg) + } + } + + // Log the changes, if there are any left. + if len(changes) > 0 { + return fmt.Sprintf("Hub Changed: %s", makeHubName(to.Name, to.ID)), + strings.Join(changes, "\n"), + nil + } + } + + return "", "", errNoChanges +} + +func formatChange(change diff.Change, fullPath string) string { + switch { + case strings.HasPrefix(fullPath, "States"): + switch change.Type { + case diff.CREATE: + return formatState(fmt.Sprintf("%v", change.To), true) + case diff.UPDATE: + a := formatState(fmt.Sprintf("%v", change.To), true) + b := formatState(fmt.Sprintf("%v", change.From), false) + switch { + case a != "" && b != "": + return a + "\n" + b + case a != "": + return a + case b != "": + return b + } + case diff.DELETE: + return formatState(fmt.Sprintf("%v", change.From), false) + } + + default: + switch change.Type { + case diff.CREATE: + return fmt.Sprintf("%s added %v", fullPath, change.To) + case diff.UPDATE: + return fmt.Sprintf("%s changed from %v to %v", fullPath, change.From, change.To) + case diff.DELETE: + return fmt.Sprintf("%s removed %v", fullPath, change.From) + } + } + + return "" +} + +func formatState(name string, isSet bool) string { + // Check if state should be ignored. + if !reportAllChanges && slices.Contains[[]string, string](ignoreStates, name) { + return "" + } + + if isSet { + return fmt.Sprintf("State is %v", name) + } + return fmt.Sprintf("State is NOT %v", name) +} + +func makeHubName(name, id string) string { + shortenedID := id[len(id)-8:len(id)-4] + + "-" + + id[len(id)-4:] + + // Be more careful, as the Hub name is user input. + switch { + case name == "": + return shortenedID + case len(name) > 16: + return fmt.Sprintf("%s (%s)", name[:16], shortenedID) + default: + return fmt.Sprintf("%s (%s)", name, shortenedID) + } +} diff --git a/cmds/portmaster-core/main.go b/cmds/portmaster-core/main.go index 517f604a..62d05e5d 100644 --- a/cmds/portmaster-core/main.go +++ b/cmds/portmaster-core/main.go @@ -10,16 +10,16 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/metrics" "github.com/safing/portbase/run" - "github.com/safing/portmaster/updates" - "github.com/safing/spn/conf" + "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/spn/conf" // Include packages here. _ "github.com/safing/portbase/modules/subsystems" - _ "github.com/safing/portmaster/core" - _ "github.com/safing/portmaster/firewall" - _ "github.com/safing/portmaster/nameserver" - _ "github.com/safing/portmaster/ui" - _ "github.com/safing/spn/captain" + _ "github.com/safing/portmaster/service/core" + _ "github.com/safing/portmaster/service/firewall" + _ "github.com/safing/portmaster/service/nameserver" + _ "github.com/safing/portmaster/service/ui" + _ "github.com/safing/portmaster/spn/captain" ) func main() { diff --git a/cmds/portmaster-start/main.go b/cmds/portmaster-start/main.go index 246b2075..1e0c66aa 100644 --- a/cmds/portmaster-start/main.go +++ b/cmds/portmaster-start/main.go @@ -20,7 +20,7 @@ import ( portlog "github.com/safing/portbase/log" "github.com/safing/portbase/updater" "github.com/safing/portbase/utils" - "github.com/safing/portmaster/updates/helper" + "github.com/safing/portmaster/service/updates/helper" ) var ( diff --git a/cmds/portmaster-start/recover_linux.go b/cmds/portmaster-start/recover_linux.go index ecb9a219..96719bd8 100644 --- a/cmds/portmaster-start/recover_linux.go +++ b/cmds/portmaster-start/recover_linux.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/go-multierror" "github.com/spf13/cobra" - "github.com/safing/portmaster/firewall/interception" + "github.com/safing/portmaster/service/firewall/interception" ) var recoverIPTablesCmd = &cobra.Command{ diff --git a/cmds/portmaster-start/run.go b/cmds/portmaster-start/run.go index 0536f30a..12606d8e 100644 --- a/cmds/portmaster-start/run.go +++ b/cmds/portmaster-start/run.go @@ -16,7 +16,7 @@ import ( "github.com/spf13/cobra" "github.com/tevino/abool" - "github.com/safing/portmaster/updates/helper" + "github.com/safing/portmaster/service/updates/helper" ) const ( diff --git a/cmds/portmaster-start/show.go b/cmds/portmaster-start/show.go index 207b7183..7ae6fc85 100644 --- a/cmds/portmaster-start/show.go +++ b/cmds/portmaster-start/show.go @@ -6,7 +6,7 @@ import ( "github.com/spf13/cobra" - "github.com/safing/portmaster/updates/helper" + "github.com/safing/portmaster/service/updates/helper" ) func init() { diff --git a/cmds/portmaster-start/update.go b/cmds/portmaster-start/update.go index 805e0cae..bbcec860 100644 --- a/cmds/portmaster-start/update.go +++ b/cmds/portmaster-start/update.go @@ -10,7 +10,7 @@ import ( portlog "github.com/safing/portbase/log" "github.com/safing/portbase/updater" - "github.com/safing/portmaster/updates/helper" + "github.com/safing/portmaster/service/updates/helper" ) var ( diff --git a/cmds/portmaster-start/verify.go b/cmds/portmaster-start/verify.go index ded921b8..7fb7be08 100644 --- a/cmds/portmaster-start/verify.go +++ b/cmds/portmaster-start/verify.go @@ -15,7 +15,7 @@ import ( "github.com/safing/jess/filesig" portlog "github.com/safing/portbase/log" "github.com/safing/portbase/updater" - "github.com/safing/portmaster/updates/helper" + "github.com/safing/portmaster/service/updates/helper" ) var ( diff --git a/cmds/testsuite/.gitignore b/cmds/testsuite/.gitignore new file mode 100644 index 00000000..08e00271 --- /dev/null +++ b/cmds/testsuite/.gitignore @@ -0,0 +1,3 @@ +# Compiled binaries +testsuite +testsuite.exe diff --git a/cmds/testsuite/db.go b/cmds/testsuite/db.go new file mode 100644 index 00000000..848e4d89 --- /dev/null +++ b/cmds/testsuite/db.go @@ -0,0 +1,33 @@ +package main + +import ( + "github.com/safing/portbase/database" + _ "github.com/safing/portbase/database/storage/hashmap" +) + +func setupDatabases(path string) error { + err := database.InitializeWithPath(path) + if err != nil { + return err + } + + _, err = database.Register(&database.Database{ + Name: "core", + Description: "Holds core data, such as settings and profiles", + StorageType: "hashmap", + }) + if err != nil { + return err + } + + _, err = database.Register(&database.Database{ + Name: "cache", + Description: "Cached data, such as Intelligence and DNS Records", + StorageType: "hashmap", + }) + if err != nil { + return err + } + + return nil +} diff --git a/cmds/testsuite/login.go b/cmds/testsuite/login.go new file mode 100644 index 00000000..bf1e5ef3 --- /dev/null +++ b/cmds/testsuite/login.go @@ -0,0 +1,125 @@ +package main + +import ( + "errors" + "fmt" + "log" + + "github.com/spf13/cobra" + + "github.com/safing/portmaster/spn/access" + "github.com/safing/portmaster/spn/access/account" +) + +var ( + loginCmd = &cobra.Command{ + Use: "login", + Short: "Test login and token issuing", + RunE: runTestCommand(testLogin), + } + + loginUsername string + loginPassword string + loginDeviceID string +) + +func init() { + rootCmd.AddCommand(loginCmd) + + // Add flags for login options. + flags := loginCmd.Flags() + flags.StringVar(&loginUsername, "username", "", "set username to use for the login test") + flags.StringVar(&loginPassword, "password", "", "set password to use for the login test") + flags.StringVar(&loginDeviceID, "device-id", "", "set device ID to use for the login test") + + // Mark all as required. + _ = loginCmd.MarkFlagRequired("username") + _ = loginCmd.MarkFlagRequired("password") + _ = loginCmd.MarkFlagRequired("device-id") +} + +func testLogin(cmd *cobra.Command, args []string) (err error) { + // Init token zones. + err = access.InitializeZones() + if err != nil { + return fmt.Errorf("failed to initialize token zones: %w", err) + } + + // Set initial user object in order to set the device ID that should be used for login. + initialUser := &access.UserRecord{ + User: &account.User{ + Username: loginUsername, + Device: &account.Device{ + ID: loginDeviceID, + }, + }, + } + err = initialUser.Save() + if err != nil { + return fmt.Errorf("failed to save initial user with device ID: %w", err) + } + + // Login. + _, _, err = access.Login(loginUsername, loginPassword) + if err != nil { + return fmt.Errorf("login failed: %w", err) + } + + // Check user. + user, err := access.GetUser() + if err != nil { + return fmt.Errorf("failed to get user after login: %w", err) + } + if verbose { + log.Printf("user (from login): %+v", user.User) + log.Printf("device (from login): %+v", user.User.Device) + } + + // Check if the device ID is unchanged. + if user.Device.ID != loginDeviceID { + return errors.New("device ID changed") + } + + // Check Auth Token. + authToken, err := access.GetAuthToken() + if err != nil { + return fmt.Errorf("failed to get auth token after login: %w", err) + } + if verbose { + log.Printf("auth token (from login): %+v", authToken.Token) + } + firstAuthToken := authToken.Token.Token + + // Update User. + _, _, err = access.UpdateUser() + if err != nil { + return fmt.Errorf("failed to update user: %w", err) + } + + // Check if we received a new Auth Token. + authToken, err = access.GetAuthToken() + if err != nil { + return fmt.Errorf("failed to get auth token after user update: %w", err) + } + if verbose { + log.Printf("auth token (from update): %+v", authToken.Token) + } + if authToken.Token.Token == firstAuthToken { + return errors.New("auth token did not change after update") + } + + // Get Tokens. + err = access.UpdateTokens() + if err != nil { + return fmt.Errorf("failed to update tokens: %w", err) + } + regular, fallback := access.GetTokenAmount(access.ExpandAndConnectZones) + if verbose { + log.Printf("received tokens: %d regular, %d fallback", regular, fallback) + } + if regular == 0 || fallback == 0 { + return fmt.Errorf("not enough tokens after fetching: %d regular, %d fallback", regular, fallback) + } + + return nil +} diff --git a/cmds/testsuite/main.go b/cmds/testsuite/main.go new file mode 100644 index 00000000..d4edcead --- /dev/null +++ b/cmds/testsuite/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/spf13/cobra" +) + +var ( + rootCmd = &cobra.Command{ + Use: "testsuite", + Short: "An integration and end-to-end test tool for the SPN", + } + + verbose bool +) + +func runTestCommand(cmdFunc func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + // Setup + dbDir, err := os.MkdirTemp("", "spn-testsuite-") + if err != nil { + makeReports(cmd, fmt.Errorf("internal test error: failed to setup datbases: %w", err)) + return err + } + if err = setupDatabases(dbDir); err != nil { + makeReports(cmd, fmt.Errorf("internal test error: failed to setup datbases: %w", err)) + return err + } + + // Run Test + err = cmdFunc(cmd, args) + if err != nil { + log.Printf("test failed: %s", err) + } + + // Report + makeReports(cmd, err) + + // Cleanup and return more important error. + cleanUpErr := os.RemoveAll(dbDir) + if cleanUpErr != nil { + // Only log if the test failed, so we can return the more important error + if err == nil { + return cleanUpErr + } + log.Printf("cleanup failed: %s", err) + } + + return err + } +} + +func makeReports(cmd *cobra.Command, err error) { + reportToHealthCheckIfEnabled(cmd, err) +} + +func init() { + flags := rootCmd.PersistentFlags() + flags.BoolVarP(&verbose, "verbose", "v", false, "enable verbose logging") +} + +func main() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/cmds/testsuite/report_healthcheck.go b/cmds/testsuite/report_healthcheck.go new file mode 100644 index 00000000..4ca9eb4a --- /dev/null +++ b/cmds/testsuite/report_healthcheck.go @@ -0,0 +1,51 @@ +package main + +import ( + "log" + "net/http" + "strings" + + "github.com/spf13/cobra" +) + +var healthCheckReportURL string + +func init() { + flags := rootCmd.PersistentFlags() + flags.StringVar(&healthCheckReportURL, "report-to-healthcheck", "", "report to the given healthchecks URL") +} + +func reportToHealthCheckIfEnabled(_ *cobra.Command, failureErr error) { + if healthCheckReportURL == "" { + return + } + + if failureErr != nil { + // Report failure. + resp, err := http.Post( + healthCheckReportURL+"/fail", + "text/plain; utf-8", + strings.NewReader(failureErr.Error()), + ) + if err != nil { + log.Printf("failed to report failure to healthcheck at %q: %s", healthCheckReportURL, err) + return + } + _ = resp.Body.Close() + + // Always log that we've report the error. + log.Printf("reported failure to healthcheck at %q", healthCheckReportURL) + } else { + // Report success. + resp, err := http.Get(healthCheckReportURL) //nolint:gosec + if err != nil { + log.Printf("failed to report success to healthcheck at %q: %s", healthCheckReportURL, err) + return + } + _ = resp.Body.Close() + + if verbose { + log.Printf("reported success to healthcheck at %q", healthCheckReportURL) + } + } +} diff --git a/cmds/winkext-test/main.go b/cmds/winkext-test/main.go index 8c7bf2cf..0a3d8c4b 100644 --- a/cmds/winkext-test/main.go +++ b/cmds/winkext-test/main.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package main @@ -11,8 +12,8 @@ import ( "syscall" "github.com/safing/portbase/log" - "github.com/safing/portmaster/firewall/interception/windowskext" - "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/service/firewall/interception/windowskext" + "github.com/safing/portmaster/service/network/packet" ) var ( diff --git a/desktop/angular/.gitkeep b/desktop/angular/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/desktop/tauri/.gitkeep b/desktop/tauri/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/go.mod b/go.mod index 9421ce7f..6a1fc4e3 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ replace github.com/tc-hib/winres => github.com/dhaavi/winres v0.2.2 require ( github.com/Xuanwo/go-locale v1.1.0 github.com/agext/levenshtein v1.2.3 + github.com/awalterschulze/gographviz v2.0.3+incompatible github.com/cilium/ebpf v0.12.3 github.com/coreos/go-iptables v0.7.0 github.com/florianl/go-conntrack v0.4.0 @@ -25,11 +26,13 @@ require ( github.com/mat/besticon v3.12.0+incompatible github.com/miekg/dns v1.1.57 github.com/mitchellh/go-server-timing v1.0.1 + github.com/mr-tron/base58 v1.2.0 github.com/oschwald/maxminddb-golang v1.12.0 + github.com/r3labs/diff/v3 v3.0.1 + github.com/rot256/pblind v0.0.0-20231024115251-cd3f239f28c1 github.com/safing/jess v0.3.3 github.com/safing/portbase v0.18.9 github.com/safing/portmaster-android/go v0.0.0-20230830120134-3226ceac3bec - github.com/safing/spn v0.7.5 github.com/shirou/gopsutil v3.21.11+incompatible github.com/spf13/cobra v1.8.0 github.com/spkg/zipfs v0.7.1 @@ -53,8 +56,8 @@ require ( github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 // indirect github.com/alessio/shellescape v1.4.2 // indirect github.com/armon/go-radix v1.0.0 // indirect - github.com/awalterschulze/gographviz v2.0.3+incompatible // indirect github.com/bluele/gcache v0.0.2 // indirect + github.com/brianvoe/gofakeit v3.18.0+incompatible // indirect github.com/danieljoos/wincred v1.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -79,12 +82,10 @@ require ( github.com/mdlayher/socket v0.5.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/mr-tron/base58 v1.2.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/rot256/pblind v0.0.0-20231024115251-cd3f239f28c1 // indirect github.com/satori/go.uuid v1.2.0 // indirect github.com/seehuhn/fortuna v1.0.1 // indirect github.com/seehuhn/sha256d v1.0.0 // indirect diff --git a/go.sum b/go.sum index d9fdd161..e4c9c316 100644 --- a/go.sum +++ b/go.sum @@ -203,6 +203,8 @@ 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/r3labs/diff/v3 v3.0.1 h1:CBKqf3XmNRHXKmdU7mZP1w7TV0pDyVCis1AUHtA4Xtg= +github.com/r3labs/diff/v3 v3.0.1/go.mod h1:f1S9bourRbiM66NskseyUdo0fTmEE0qKrikYJX63dgo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= @@ -246,6 +248,7 @@ github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -275,6 +278,7 @@ github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OL github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI= github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= diff --git a/pack b/pack index 493b77f3..e23f1286 100755 --- a/pack +++ b/pack @@ -24,16 +24,19 @@ function safe_execute { function check { ./cmds/portmaster-core/pack check ./cmds/portmaster-start/pack check + ./cmds/hub/pack check } function build { safe_execute ./cmds/portmaster-core/pack build safe_execute ./cmds/portmaster-start/pack build + safe_execute ./cmds/hub/pack build } function reset { ./cmds/portmaster-core/pack reset ./cmds/portmaster-start/pack reset + ./cmds/hub/pack resset } case $1 in diff --git a/packaging/linux/.gitkeep b/packaging/linux/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packaging/windows/.gitkeep b/packaging/windows/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/runtime/.gitkeep b/runtime/.gitkeep new file mode 100644 index 00000000..0d64ac96 --- /dev/null +++ b/runtime/.gitkeep @@ -0,0 +1 @@ +The new portbase should land here. \ No newline at end of file diff --git a/broadcasts/api.go b/service/broadcasts/api.go similarity index 100% rename from broadcasts/api.go rename to service/broadcasts/api.go diff --git a/broadcasts/data.go b/service/broadcasts/data.go similarity index 91% rename from broadcasts/data.go rename to service/broadcasts/data.go index a04c7820..22faf458 100644 --- a/broadcasts/data.go +++ b/service/broadcasts/data.go @@ -5,12 +5,12 @@ import ( "time" "github.com/safing/portbase/config" - "github.com/safing/portmaster/intel/geoip" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/updates" - "github.com/safing/spn/access" - "github.com/safing/spn/access/account" - "github.com/safing/spn/captain" + "github.com/safing/portmaster/service/intel/geoip" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/spn/access" + "github.com/safing/portmaster/spn/access/account" + "github.com/safing/portmaster/spn/captain" ) var portmasterStarted = time.Now() diff --git a/broadcasts/install_info.go b/service/broadcasts/install_info.go similarity index 100% rename from broadcasts/install_info.go rename to service/broadcasts/install_info.go diff --git a/broadcasts/module.go b/service/broadcasts/module.go similarity index 100% rename from broadcasts/module.go rename to service/broadcasts/module.go diff --git a/broadcasts/notify.go b/service/broadcasts/notify.go similarity index 99% rename from broadcasts/notify.go rename to service/broadcasts/notify.go index cd6c38f2..4e359139 100644 --- a/broadcasts/notify.go +++ b/service/broadcasts/notify.go @@ -18,7 +18,7 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/modules" "github.com/safing/portbase/notifications" - "github.com/safing/portmaster/updates" + "github.com/safing/portmaster/service/updates" ) const ( diff --git a/broadcasts/state.go b/service/broadcasts/state.go similarity index 100% rename from broadcasts/state.go rename to service/broadcasts/state.go diff --git a/broadcasts/testdata/README.md b/service/broadcasts/testdata/README.md similarity index 100% rename from broadcasts/testdata/README.md rename to service/broadcasts/testdata/README.md diff --git a/broadcasts/testdata/notifications.yaml b/service/broadcasts/testdata/notifications.yaml similarity index 100% rename from broadcasts/testdata/notifications.yaml rename to service/broadcasts/testdata/notifications.yaml diff --git a/compat/api.go b/service/compat/api.go similarity index 100% rename from compat/api.go rename to service/compat/api.go diff --git a/compat/callbacks.go b/service/compat/callbacks.go similarity index 90% rename from compat/callbacks.go rename to service/compat/callbacks.go index e997ff8f..2abfa858 100644 --- a/compat/callbacks.go +++ b/service/compat/callbacks.go @@ -3,8 +3,8 @@ package compat import ( "net" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/process" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/process" ) // SubmitSystemIntegrationCheckPacket submit a packet for the system integrity check. diff --git a/compat/debug_default.go b/service/compat/debug_default.go similarity index 100% rename from compat/debug_default.go rename to service/compat/debug_default.go diff --git a/compat/debug_linux.go b/service/compat/debug_linux.go similarity index 100% rename from compat/debug_linux.go rename to service/compat/debug_linux.go diff --git a/compat/debug_windows.go b/service/compat/debug_windows.go similarity index 100% rename from compat/debug_windows.go rename to service/compat/debug_windows.go diff --git a/compat/iptables.go b/service/compat/iptables.go similarity index 100% rename from compat/iptables.go rename to service/compat/iptables.go diff --git a/compat/iptables_test.go b/service/compat/iptables_test.go similarity index 100% rename from compat/iptables_test.go rename to service/compat/iptables_test.go diff --git a/compat/module.go b/service/compat/module.go similarity index 97% rename from compat/module.go rename to service/compat/module.go index c159d02f..b8b95090 100644 --- a/compat/module.go +++ b/service/compat/module.go @@ -9,8 +9,8 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/modules" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/resolver" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/resolver" ) var ( diff --git a/compat/notify.go b/service/compat/notify.go similarity index 98% rename from compat/notify.go rename to service/compat/notify.go index 39157648..f26f0ea3 100644 --- a/compat/notify.go +++ b/service/compat/notify.go @@ -12,8 +12,8 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/modules" "github.com/safing/portbase/notifications" - "github.com/safing/portmaster/process" - "github.com/safing/portmaster/profile" + "github.com/safing/portmaster/service/process" + "github.com/safing/portmaster/service/profile" ) type baseIssue struct { diff --git a/compat/selfcheck.go b/service/compat/selfcheck.go similarity index 97% rename from compat/selfcheck.go rename to service/compat/selfcheck.go index e26c1ed5..f4775cdc 100644 --- a/compat/selfcheck.go +++ b/service/compat/selfcheck.go @@ -12,9 +12,9 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/rng" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/resolver" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/resolver" ) var ( diff --git a/compat/wfpstate.go b/service/compat/wfpstate.go similarity index 100% rename from compat/wfpstate.go rename to service/compat/wfpstate.go diff --git a/compat/wfpstate_test.go b/service/compat/wfpstate_test.go similarity index 100% rename from compat/wfpstate_test.go rename to service/compat/wfpstate_test.go diff --git a/core/api.go b/service/core/api.go similarity index 96% rename from core/api.go rename to service/core/api.go index 6a653909..8e7d24bc 100644 --- a/core/api.go +++ b/service/core/api.go @@ -15,12 +15,12 @@ import ( "github.com/safing/portbase/notifications" "github.com/safing/portbase/rng" "github.com/safing/portbase/utils/debug" - "github.com/safing/portmaster/compat" - "github.com/safing/portmaster/process" - "github.com/safing/portmaster/resolver" - "github.com/safing/portmaster/status" - "github.com/safing/portmaster/updates" - "github.com/safing/spn/captain" + "github.com/safing/portmaster/service/compat" + "github.com/safing/portmaster/service/process" + "github.com/safing/portmaster/service/resolver" + "github.com/safing/portmaster/service/status" + "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/spn/captain" ) func registerAPIEndpoints() error { diff --git a/core/base/databases.go b/service/core/base/databases.go similarity index 100% rename from core/base/databases.go rename to service/core/base/databases.go diff --git a/core/base/global.go b/service/core/base/global.go similarity index 100% rename from core/base/global.go rename to service/core/base/global.go diff --git a/core/base/logs.go b/service/core/base/logs.go similarity index 100% rename from core/base/logs.go rename to service/core/base/logs.go diff --git a/core/base/module.go b/service/core/base/module.go similarity index 100% rename from core/base/module.go rename to service/core/base/module.go diff --git a/core/base/profiling.go b/service/core/base/profiling.go similarity index 100% rename from core/base/profiling.go rename to service/core/base/profiling.go diff --git a/core/config.go b/service/core/config.go similarity index 100% rename from core/config.go rename to service/core/config.go diff --git a/core/core.go b/service/core/core.go similarity index 84% rename from core/core.go rename to service/core/core.go index d0c95418..ff535759 100644 --- a/core/core.go +++ b/service/core/core.go @@ -9,13 +9,13 @@ import ( "github.com/safing/portbase/metrics" "github.com/safing/portbase/modules" "github.com/safing/portbase/modules/subsystems" - _ "github.com/safing/portmaster/broadcasts" - _ "github.com/safing/portmaster/netenv" - _ "github.com/safing/portmaster/netquery" - _ "github.com/safing/portmaster/status" - _ "github.com/safing/portmaster/sync" - _ "github.com/safing/portmaster/ui" - "github.com/safing/portmaster/updates" + _ "github.com/safing/portmaster/service/broadcasts" + _ "github.com/safing/portmaster/service/netenv" + _ "github.com/safing/portmaster/service/netquery" + _ "github.com/safing/portmaster/service/status" + _ "github.com/safing/portmaster/service/sync" + _ "github.com/safing/portmaster/service/ui" + "github.com/safing/portmaster/service/updates" ) const ( diff --git a/core/os_default.go b/service/core/os_default.go similarity index 100% rename from core/os_default.go rename to service/core/os_default.go diff --git a/core/os_windows.go b/service/core/os_windows.go similarity index 100% rename from core/os_windows.go rename to service/core/os_windows.go diff --git a/core/pmtesting/testing.go b/service/core/pmtesting/testing.go similarity index 96% rename from core/pmtesting/testing.go rename to service/core/pmtesting/testing.go index 9b597c83..16253f86 100644 --- a/core/pmtesting/testing.go +++ b/service/core/pmtesting/testing.go @@ -7,7 +7,7 @@ // import ( // "testing" // -// "github.com/safing/portmaster/core/pmtesting" +// "github.com/safing/portmaster/service/core/pmtesting" // ) // // func TestMain(m *testing.M) { @@ -27,7 +27,7 @@ import ( "github.com/safing/portbase/dataroot" "github.com/safing/portbase/log" "github.com/safing/portbase/modules" - "github.com/safing/portmaster/core/base" + "github.com/safing/portmaster/service/core/base" ) var printStackOnExit bool diff --git a/detection/dga/lms.go b/service/detection/dga/lms.go similarity index 100% rename from detection/dga/lms.go rename to service/detection/dga/lms.go diff --git a/detection/dga/lms_test.go b/service/detection/dga/lms_test.go similarity index 100% rename from detection/dga/lms_test.go rename to service/detection/dga/lms_test.go diff --git a/firewall/api.go b/service/firewall/api.go similarity index 96% rename from firewall/api.go rename to service/firewall/api.go index b17efe6d..949e168f 100644 --- a/firewall/api.go +++ b/service/firewall/api.go @@ -13,11 +13,11 @@ import ( "github.com/safing/portbase/dataroot" "github.com/safing/portbase/log" "github.com/safing/portbase/utils" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/network/netutils" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/process" - "github.com/safing/portmaster/updates" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/process" + "github.com/safing/portmaster/service/updates" ) const ( diff --git a/firewall/bypassing.go b/service/firewall/bypassing.go similarity index 87% rename from firewall/bypassing.go rename to service/firewall/bypassing.go index cf8502cb..415fc6c8 100644 --- a/firewall/bypassing.go +++ b/service/firewall/bypassing.go @@ -4,11 +4,11 @@ import ( "context" "strings" - "github.com/safing/portmaster/compat" - "github.com/safing/portmaster/nameserver/nsutil" - "github.com/safing/portmaster/network" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/profile/endpoints" + "github.com/safing/portmaster/service/compat" + "github.com/safing/portmaster/service/nameserver/nsutil" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/profile/endpoints" ) var resolverFilterLists = []string{"17-DNS"} diff --git a/firewall/config.go b/service/firewall/config.go similarity index 98% rename from firewall/config.go rename to service/firewall/config.go index 4e3ca653..960c000b 100644 --- a/firewall/config.go +++ b/service/firewall/config.go @@ -6,8 +6,8 @@ import ( "github.com/safing/portbase/api" "github.com/safing/portbase/config" "github.com/safing/portbase/notifications" - "github.com/safing/portmaster/core" - "github.com/safing/spn/captain" + "github.com/safing/portmaster/service/core" + "github.com/safing/portmaster/spn/captain" ) // Configuration Keys. diff --git a/firewall/dns.go b/service/firewall/dns.go similarity index 97% rename from firewall/dns.go rename to service/firewall/dns.go index b0ac071a..3712165d 100644 --- a/firewall/dns.go +++ b/service/firewall/dns.go @@ -10,11 +10,11 @@ import ( "github.com/safing/portbase/database" "github.com/safing/portbase/log" - "github.com/safing/portmaster/network" - "github.com/safing/portmaster/network/netutils" - "github.com/safing/portmaster/profile" - "github.com/safing/portmaster/profile/endpoints" - "github.com/safing/portmaster/resolver" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/service/profile" + "github.com/safing/portmaster/service/profile/endpoints" + "github.com/safing/portmaster/service/resolver" ) func filterDNSSection( diff --git a/firewall/inspection/inspection.go b/service/firewall/inspection/inspection.go similarity index 95% rename from firewall/inspection/inspection.go rename to service/firewall/inspection/inspection.go index 92de0345..44855ba4 100644 --- a/firewall/inspection/inspection.go +++ b/service/firewall/inspection/inspection.go @@ -3,8 +3,8 @@ package inspection import ( "sync" - "github.com/safing/portmaster/network" - "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/network/packet" ) //nolint:golint,stylecheck // FIXME diff --git a/firewall/interception/ebpf/bandwidth/bpf_bpfeb.go b/service/firewall/interception/ebpf/bandwidth/bpf_bpfeb.go similarity index 100% rename from firewall/interception/ebpf/bandwidth/bpf_bpfeb.go rename to service/firewall/interception/ebpf/bandwidth/bpf_bpfeb.go diff --git a/firewall/interception/ebpf/bandwidth/bpf_bpfeb.o b/service/firewall/interception/ebpf/bandwidth/bpf_bpfeb.o similarity index 100% rename from firewall/interception/ebpf/bandwidth/bpf_bpfeb.o rename to service/firewall/interception/ebpf/bandwidth/bpf_bpfeb.o diff --git a/firewall/interception/ebpf/bandwidth/bpf_bpfel.go b/service/firewall/interception/ebpf/bandwidth/bpf_bpfel.go similarity index 100% rename from firewall/interception/ebpf/bandwidth/bpf_bpfel.go rename to service/firewall/interception/ebpf/bandwidth/bpf_bpfel.go diff --git a/firewall/interception/ebpf/bandwidth/bpf_bpfel.o b/service/firewall/interception/ebpf/bandwidth/bpf_bpfel.o similarity index 100% rename from firewall/interception/ebpf/bandwidth/bpf_bpfel.o rename to service/firewall/interception/ebpf/bandwidth/bpf_bpfel.o diff --git a/firewall/interception/ebpf/bandwidth/interface.go b/service/firewall/interception/ebpf/bandwidth/interface.go similarity index 98% rename from firewall/interception/ebpf/bandwidth/interface.go rename to service/firewall/interception/ebpf/bandwidth/interface.go index 3a08bbad..e1473dbd 100644 --- a/firewall/interception/ebpf/bandwidth/interface.go +++ b/service/firewall/interception/ebpf/bandwidth/interface.go @@ -16,7 +16,7 @@ import ( "golang.org/x/sys/unix" "github.com/safing/portbase/log" - "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/service/network/packet" ) //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -cflags "-O2 -g -Wall -Werror" bpf ../programs/bandwidth.c diff --git a/firewall/interception/ebpf/connection_listener/bpf_bpfeb.go b/service/firewall/interception/ebpf/connection_listener/bpf_bpfeb.go similarity index 100% rename from firewall/interception/ebpf/connection_listener/bpf_bpfeb.go rename to service/firewall/interception/ebpf/connection_listener/bpf_bpfeb.go diff --git a/firewall/interception/ebpf/connection_listener/bpf_bpfeb.o b/service/firewall/interception/ebpf/connection_listener/bpf_bpfeb.o similarity index 100% rename from firewall/interception/ebpf/connection_listener/bpf_bpfeb.o rename to service/firewall/interception/ebpf/connection_listener/bpf_bpfeb.o diff --git a/firewall/interception/ebpf/connection_listener/bpf_bpfel.go b/service/firewall/interception/ebpf/connection_listener/bpf_bpfel.go similarity index 100% rename from firewall/interception/ebpf/connection_listener/bpf_bpfel.go rename to service/firewall/interception/ebpf/connection_listener/bpf_bpfel.go diff --git a/firewall/interception/ebpf/connection_listener/bpf_bpfel.o b/service/firewall/interception/ebpf/connection_listener/bpf_bpfel.o similarity index 100% rename from firewall/interception/ebpf/connection_listener/bpf_bpfel.o rename to service/firewall/interception/ebpf/connection_listener/bpf_bpfel.o diff --git a/firewall/interception/ebpf/connection_listener/worker.go b/service/firewall/interception/ebpf/connection_listener/worker.go similarity index 98% rename from firewall/interception/ebpf/connection_listener/worker.go rename to service/firewall/interception/ebpf/connection_listener/worker.go index bee03f12..aadfd57f 100644 --- a/firewall/interception/ebpf/connection_listener/worker.go +++ b/service/firewall/interception/ebpf/connection_listener/worker.go @@ -15,7 +15,7 @@ import ( "github.com/cilium/ebpf/rlimit" "github.com/safing/portbase/log" - "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/service/network/packet" ) //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -cflags "-O2 -g -Wall -Werror" -type Event bpf ../programs/monitor.c diff --git a/firewall/interception/ebpf/exec/bpf_bpfeb.go b/service/firewall/interception/ebpf/exec/bpf_bpfeb.go similarity index 100% rename from firewall/interception/ebpf/exec/bpf_bpfeb.go rename to service/firewall/interception/ebpf/exec/bpf_bpfeb.go diff --git a/firewall/interception/ebpf/exec/bpf_bpfeb.o b/service/firewall/interception/ebpf/exec/bpf_bpfeb.o similarity index 100% rename from firewall/interception/ebpf/exec/bpf_bpfeb.o rename to service/firewall/interception/ebpf/exec/bpf_bpfeb.o diff --git a/firewall/interception/ebpf/exec/bpf_bpfel.go b/service/firewall/interception/ebpf/exec/bpf_bpfel.go similarity index 100% rename from firewall/interception/ebpf/exec/bpf_bpfel.go rename to service/firewall/interception/ebpf/exec/bpf_bpfel.go diff --git a/firewall/interception/ebpf/exec/bpf_bpfel.o b/service/firewall/interception/ebpf/exec/bpf_bpfel.o similarity index 100% rename from firewall/interception/ebpf/exec/bpf_bpfel.o rename to service/firewall/interception/ebpf/exec/bpf_bpfel.o diff --git a/firewall/interception/ebpf/exec/exec.go b/service/firewall/interception/ebpf/exec/exec.go similarity index 100% rename from firewall/interception/ebpf/exec/exec.go rename to service/firewall/interception/ebpf/exec/exec.go diff --git a/firewall/interception/ebpf/programs/bandwidth.c b/service/firewall/interception/ebpf/programs/bandwidth.c similarity index 100% rename from firewall/interception/ebpf/programs/bandwidth.c rename to service/firewall/interception/ebpf/programs/bandwidth.c diff --git a/firewall/interception/ebpf/programs/bpf/bpf_core_read.h b/service/firewall/interception/ebpf/programs/bpf/bpf_core_read.h similarity index 100% rename from firewall/interception/ebpf/programs/bpf/bpf_core_read.h rename to service/firewall/interception/ebpf/programs/bpf/bpf_core_read.h diff --git a/firewall/interception/ebpf/programs/bpf/bpf_helper_defs.h b/service/firewall/interception/ebpf/programs/bpf/bpf_helper_defs.h similarity index 100% rename from firewall/interception/ebpf/programs/bpf/bpf_helper_defs.h rename to service/firewall/interception/ebpf/programs/bpf/bpf_helper_defs.h diff --git a/firewall/interception/ebpf/programs/bpf/bpf_helpers.h b/service/firewall/interception/ebpf/programs/bpf/bpf_helpers.h similarity index 100% rename from firewall/interception/ebpf/programs/bpf/bpf_helpers.h rename to service/firewall/interception/ebpf/programs/bpf/bpf_helpers.h diff --git a/firewall/interception/ebpf/programs/bpf/bpf_tracing.h b/service/firewall/interception/ebpf/programs/bpf/bpf_tracing.h similarity index 100% rename from firewall/interception/ebpf/programs/bpf/bpf_tracing.h rename to service/firewall/interception/ebpf/programs/bpf/bpf_tracing.h diff --git a/firewall/interception/ebpf/programs/exec.c b/service/firewall/interception/ebpf/programs/exec.c similarity index 100% rename from firewall/interception/ebpf/programs/exec.c rename to service/firewall/interception/ebpf/programs/exec.c diff --git a/firewall/interception/ebpf/programs/monitor.c b/service/firewall/interception/ebpf/programs/monitor.c similarity index 100% rename from firewall/interception/ebpf/programs/monitor.c rename to service/firewall/interception/ebpf/programs/monitor.c diff --git a/firewall/interception/ebpf/programs/update.sh b/service/firewall/interception/ebpf/programs/update.sh similarity index 100% rename from firewall/interception/ebpf/programs/update.sh rename to service/firewall/interception/ebpf/programs/update.sh diff --git a/firewall/interception/ebpf/programs/vmlinux-x86.h b/service/firewall/interception/ebpf/programs/vmlinux-x86.h similarity index 100% rename from firewall/interception/ebpf/programs/vmlinux-x86.h rename to service/firewall/interception/ebpf/programs/vmlinux-x86.h diff --git a/firewall/interception/interception_default.go b/service/firewall/interception/interception_default.go similarity index 87% rename from firewall/interception/interception_default.go rename to service/firewall/interception/interception_default.go index 222a041c..a4a93f44 100644 --- a/firewall/interception/interception_default.go +++ b/service/firewall/interception/interception_default.go @@ -4,8 +4,8 @@ package interception import ( "github.com/safing/portbase/log" - "github.com/safing/portmaster/network" - "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/network/packet" ) // start starts the interception. diff --git a/firewall/interception/interception_linux.go b/service/firewall/interception/interception_linux.go similarity index 77% rename from firewall/interception/interception_linux.go rename to service/firewall/interception/interception_linux.go index 128f6649..66ca5b7e 100644 --- a/firewall/interception/interception_linux.go +++ b/service/firewall/interception/interception_linux.go @@ -4,11 +4,11 @@ import ( "context" "time" - bandwidth "github.com/safing/portmaster/firewall/interception/ebpf/bandwidth" - conn_listener "github.com/safing/portmaster/firewall/interception/ebpf/connection_listener" - "github.com/safing/portmaster/firewall/interception/nfq" - "github.com/safing/portmaster/network" - "github.com/safing/portmaster/network/packet" + bandwidth "github.com/safing/portmaster/service/firewall/interception/ebpf/bandwidth" + conn_listener "github.com/safing/portmaster/service/firewall/interception/ebpf/connection_listener" + "github.com/safing/portmaster/service/firewall/interception/nfq" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/network/packet" ) // start starts the interception. diff --git a/firewall/interception/interception_windows.go b/service/firewall/interception/interception_windows.go similarity index 88% rename from firewall/interception/interception_windows.go rename to service/firewall/interception/interception_windows.go index 069f5c01..71033c1a 100644 --- a/firewall/interception/interception_windows.go +++ b/service/firewall/interception/interception_windows.go @@ -5,10 +5,10 @@ import ( "fmt" "time" - "github.com/safing/portmaster/firewall/interception/windowskext" - "github.com/safing/portmaster/network" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/updates" + "github.com/safing/portmaster/service/firewall/interception/windowskext" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/updates" ) // start starts the interception. diff --git a/firewall/interception/introspection.go b/service/firewall/interception/introspection.go similarity index 100% rename from firewall/interception/introspection.go rename to service/firewall/interception/introspection.go diff --git a/firewall/interception/module.go b/service/firewall/interception/module.go similarity index 96% rename from firewall/interception/module.go rename to service/firewall/interception/module.go index 0b0e86d0..2802defa 100644 --- a/firewall/interception/module.go +++ b/service/firewall/interception/module.go @@ -5,7 +5,7 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/modules" - "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/service/network/packet" ) var ( diff --git a/firewall/interception/nfq/conntrack.go b/service/firewall/interception/nfq/conntrack.go similarity index 97% rename from firewall/interception/nfq/conntrack.go rename to service/firewall/interception/nfq/conntrack.go index b71651ec..6959d328 100644 --- a/firewall/interception/nfq/conntrack.go +++ b/service/firewall/interception/nfq/conntrack.go @@ -9,8 +9,8 @@ import ( ct "github.com/florianl/go-conntrack" "github.com/safing/portbase/log" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/network" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network" ) var nfct *ct.Nfct // Conntrack handler. NFCT: Network Filter Connection Tracking. diff --git a/firewall/interception/nfq/nfq.go b/service/firewall/interception/nfq/nfq.go similarity index 98% rename from firewall/interception/nfq/nfq.go rename to service/firewall/interception/nfq/nfq.go index 184e15f9..f7579920 100644 --- a/firewall/interception/nfq/nfq.go +++ b/service/firewall/interception/nfq/nfq.go @@ -15,8 +15,8 @@ import ( "golang.org/x/sys/unix" "github.com/safing/portbase/log" - pmpacket "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/process" + pmpacket "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/process" ) // Queue wraps a nfqueue. diff --git a/firewall/interception/nfq/packet.go b/service/firewall/interception/nfq/packet.go similarity index 98% rename from firewall/interception/nfq/packet.go rename to service/firewall/interception/nfq/packet.go index 8baeff5b..af3d5fac 100644 --- a/firewall/interception/nfq/packet.go +++ b/service/firewall/interception/nfq/packet.go @@ -11,7 +11,7 @@ import ( "github.com/tevino/abool" "github.com/safing/portbase/log" - pmpacket "github.com/safing/portmaster/network/packet" + pmpacket "github.com/safing/portmaster/service/network/packet" ) // Firewalling marks used by the Portmaster. diff --git a/firewall/interception/nfqueue_linux.go b/service/firewall/interception/nfqueue_linux.go similarity index 98% rename from firewall/interception/nfqueue_linux.go rename to service/firewall/interception/nfqueue_linux.go index 2e632813..537bbcb7 100644 --- a/firewall/interception/nfqueue_linux.go +++ b/service/firewall/interception/nfqueue_linux.go @@ -11,9 +11,9 @@ import ( "github.com/hashicorp/go-multierror" "github.com/safing/portbase/log" - "github.com/safing/portmaster/firewall/interception/nfq" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/service/firewall/interception/nfq" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network/packet" ) var ( diff --git a/firewall/interception/packet_tracer.go b/service/firewall/interception/packet_tracer.go similarity index 95% rename from firewall/interception/packet_tracer.go rename to service/firewall/interception/packet_tracer.go index 4d822a42..b90dfbf7 100644 --- a/firewall/interception/packet_tracer.go +++ b/service/firewall/interception/packet_tracer.go @@ -3,7 +3,7 @@ package interception import ( "time" - "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/service/network/packet" ) type tracedPacket struct { diff --git a/firewall/interception/windowskext/bandwidth_stats.go b/service/firewall/interception/windowskext/bandwidth_stats.go similarity index 98% rename from firewall/interception/windowskext/bandwidth_stats.go rename to service/firewall/interception/windowskext/bandwidth_stats.go index 2a1bddc0..f1fb856b 100644 --- a/firewall/interception/windowskext/bandwidth_stats.go +++ b/service/firewall/interception/windowskext/bandwidth_stats.go @@ -10,7 +10,7 @@ import ( "time" "github.com/safing/portbase/log" - "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/service/network/packet" ) type Rxtxdata struct { diff --git a/firewall/interception/windowskext/doc.go b/service/firewall/interception/windowskext/doc.go similarity index 100% rename from firewall/interception/windowskext/doc.go rename to service/firewall/interception/windowskext/doc.go diff --git a/firewall/interception/windowskext/handler.go b/service/firewall/interception/windowskext/handler.go similarity index 97% rename from firewall/interception/windowskext/handler.go rename to service/firewall/interception/windowskext/handler.go index f5d66761..a5a8de74 100644 --- a/firewall/interception/windowskext/handler.go +++ b/service/firewall/interception/windowskext/handler.go @@ -12,13 +12,13 @@ import ( "time" "unsafe" - "github.com/safing/portmaster/process" + "github.com/safing/portmaster/service/process" "github.com/tevino/abool" "github.com/safing/portbase/log" - "github.com/safing/portmaster/network" - "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/network/packet" ) const ( diff --git a/firewall/interception/windowskext/kext.go b/service/firewall/interception/windowskext/kext.go similarity index 98% rename from firewall/interception/windowskext/kext.go rename to service/firewall/interception/windowskext/kext.go index a7e6a1c3..7699c35a 100644 --- a/firewall/interception/windowskext/kext.go +++ b/service/firewall/interception/windowskext/kext.go @@ -11,8 +11,8 @@ import ( "unsafe" "github.com/safing/portbase/log" - "github.com/safing/portmaster/network" - "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/network/packet" "golang.org/x/sys/windows" ) diff --git a/firewall/interception/windowskext/packet.go b/service/firewall/interception/windowskext/packet.go similarity index 97% rename from firewall/interception/windowskext/packet.go rename to service/firewall/interception/windowskext/packet.go index 6c7b24da..5f96e784 100644 --- a/firewall/interception/windowskext/packet.go +++ b/service/firewall/interception/windowskext/packet.go @@ -9,8 +9,8 @@ import ( "github.com/tevino/abool" "github.com/safing/portbase/log" - "github.com/safing/portmaster/network" - "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/network/packet" ) // Packet represents an IP packet. diff --git a/firewall/interception/windowskext/service.go b/service/firewall/interception/windowskext/service.go similarity index 100% rename from firewall/interception/windowskext/service.go rename to service/firewall/interception/windowskext/service.go diff --git a/firewall/interception/windowskext/syscall.go b/service/firewall/interception/windowskext/syscall.go similarity index 100% rename from firewall/interception/windowskext/syscall.go rename to service/firewall/interception/windowskext/syscall.go diff --git a/firewall/master.go b/service/firewall/master.go similarity index 97% rename from firewall/master.go rename to service/firewall/master.go index 8c4b1e59..6549194f 100644 --- a/firewall/master.go +++ b/service/firewall/master.go @@ -12,15 +12,15 @@ import ( "golang.org/x/net/publicsuffix" "github.com/safing/portbase/log" - "github.com/safing/portmaster/detection/dga" - "github.com/safing/portmaster/intel/customlists" - "github.com/safing/portmaster/intel/filterlists" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/network" - "github.com/safing/portmaster/network/netutils" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/profile" - "github.com/safing/portmaster/profile/endpoints" + "github.com/safing/portmaster/service/detection/dga" + "github.com/safing/portmaster/service/intel/customlists" + "github.com/safing/portmaster/service/intel/filterlists" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/profile" + "github.com/safing/portmaster/service/profile/endpoints" ) const noReasonOptionKey = "" diff --git a/firewall/module.go b/service/firewall/module.go similarity index 94% rename from firewall/module.go rename to service/firewall/module.go index de6ca88a..73292967 100644 --- a/firewall/module.go +++ b/service/firewall/module.go @@ -9,11 +9,11 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/modules" "github.com/safing/portbase/modules/subsystems" - _ "github.com/safing/portmaster/core" - "github.com/safing/portmaster/network" - "github.com/safing/portmaster/profile" - "github.com/safing/spn/access" - "github.com/safing/spn/captain" + _ "github.com/safing/portmaster/service/core" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/profile" + "github.com/safing/portmaster/spn/access" + "github.com/safing/portmaster/spn/captain" ) var module *modules.Module diff --git a/firewall/packet_handler.go b/service/firewall/packet_handler.go similarity index 97% rename from firewall/packet_handler.go rename to service/firewall/packet_handler.go index 65105e15..22d9ce37 100644 --- a/firewall/packet_handler.go +++ b/service/firewall/packet_handler.go @@ -13,17 +13,17 @@ import ( "github.com/tevino/abool" "github.com/safing/portbase/log" - "github.com/safing/portmaster/compat" - _ "github.com/safing/portmaster/core/base" - "github.com/safing/portmaster/firewall/inspection" - "github.com/safing/portmaster/firewall/interception" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/netquery" - "github.com/safing/portmaster/network" - "github.com/safing/portmaster/network/netutils" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/process" - "github.com/safing/spn/access" + "github.com/safing/portmaster/service/compat" + _ "github.com/safing/portmaster/service/core/base" + "github.com/safing/portmaster/service/firewall/inspection" + "github.com/safing/portmaster/service/firewall/interception" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/netquery" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/process" + "github.com/safing/portmaster/spn/access" ) var ( diff --git a/firewall/preauth.go b/service/firewall/preauth.go similarity index 93% rename from firewall/preauth.go rename to service/firewall/preauth.go index 3ee749a6..a265350f 100644 --- a/firewall/preauth.go +++ b/service/firewall/preauth.go @@ -6,10 +6,10 @@ import ( "strconv" "sync" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/network" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/resolver" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/resolver" ) var ( diff --git a/firewall/prompt.go b/service/firewall/prompt.go similarity index 97% rename from firewall/prompt.go rename to service/firewall/prompt.go index 0b2b4ef7..51d6a12a 100644 --- a/firewall/prompt.go +++ b/service/firewall/prompt.go @@ -8,10 +8,10 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/notifications" - "github.com/safing/portmaster/intel" - "github.com/safing/portmaster/network" - "github.com/safing/portmaster/profile" - "github.com/safing/portmaster/profile/endpoints" + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/profile" + "github.com/safing/portmaster/service/profile/endpoints" ) const ( diff --git a/firewall/tunnel.go b/service/firewall/tunnel.go similarity index 91% rename from firewall/tunnel.go rename to service/firewall/tunnel.go index 013bec7b..46b5864a 100644 --- a/firewall/tunnel.go +++ b/service/firewall/tunnel.go @@ -5,18 +5,18 @@ import ( "errors" "github.com/safing/portbase/log" - "github.com/safing/portmaster/intel" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/network" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/process" - "github.com/safing/portmaster/profile" - "github.com/safing/portmaster/profile/endpoints" - "github.com/safing/portmaster/resolver" - "github.com/safing/spn/captain" - "github.com/safing/spn/crew" - "github.com/safing/spn/navigator" - "github.com/safing/spn/sluice" + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/process" + "github.com/safing/portmaster/service/profile" + "github.com/safing/portmaster/service/profile/endpoints" + "github.com/safing/portmaster/service/resolver" + "github.com/safing/portmaster/spn/captain" + "github.com/safing/portmaster/spn/crew" + "github.com/safing/portmaster/spn/navigator" + "github.com/safing/portmaster/spn/sluice" ) func checkTunneling(ctx context.Context, conn *network.Connection) { diff --git a/intel/block_reason.go b/service/intel/block_reason.go similarity index 97% rename from intel/block_reason.go rename to service/intel/block_reason.go index b29ef279..5cabbddf 100644 --- a/intel/block_reason.go +++ b/service/intel/block_reason.go @@ -9,7 +9,7 @@ import ( "github.com/miekg/dns" "github.com/safing/portbase/log" - "github.com/safing/portmaster/nameserver/nsutil" + "github.com/safing/portmaster/service/nameserver/nsutil" ) // ListMatch represents an entity that has been diff --git a/intel/customlists/config.go b/service/intel/customlists/config.go similarity index 100% rename from intel/customlists/config.go rename to service/intel/customlists/config.go diff --git a/intel/customlists/lists.go b/service/intel/customlists/lists.go similarity index 98% rename from intel/customlists/lists.go rename to service/intel/customlists/lists.go index c13a8cd5..33170dd7 100644 --- a/intel/customlists/lists.go +++ b/service/intel/customlists/lists.go @@ -12,7 +12,7 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/notifications" - "github.com/safing/portmaster/network/netutils" + "github.com/safing/portmaster/service/network/netutils" ) var ( diff --git a/intel/customlists/module.go b/service/intel/customlists/module.go similarity index 100% rename from intel/customlists/module.go rename to service/intel/customlists/module.go diff --git a/intel/entity.go b/service/intel/entity.go similarity index 98% rename from intel/entity.go rename to service/intel/entity.go index d89be9f6..5311881a 100644 --- a/intel/entity.go +++ b/service/intel/entity.go @@ -11,9 +11,9 @@ import ( "golang.org/x/net/publicsuffix" "github.com/safing/portbase/log" - "github.com/safing/portmaster/intel/filterlists" - "github.com/safing/portmaster/intel/geoip" - "github.com/safing/portmaster/network/netutils" + "github.com/safing/portmaster/service/intel/filterlists" + "github.com/safing/portmaster/service/intel/geoip" + "github.com/safing/portmaster/service/network/netutils" ) // Entity describes a remote endpoint in many different ways. diff --git a/intel/filterlists/bloom.go b/service/intel/filterlists/bloom.go similarity index 100% rename from intel/filterlists/bloom.go rename to service/intel/filterlists/bloom.go diff --git a/intel/filterlists/cache_version.go b/service/intel/filterlists/cache_version.go similarity index 100% rename from intel/filterlists/cache_version.go rename to service/intel/filterlists/cache_version.go diff --git a/intel/filterlists/database.go b/service/intel/filterlists/database.go similarity index 99% rename from intel/filterlists/database.go rename to service/intel/filterlists/database.go index 73330440..8b08f323 100644 --- a/intel/filterlists/database.go +++ b/service/intel/filterlists/database.go @@ -15,7 +15,7 @@ import ( "github.com/safing/portbase/database/record" "github.com/safing/portbase/log" "github.com/safing/portbase/updater" - "github.com/safing/portmaster/updates" + "github.com/safing/portmaster/service/updates" ) const ( diff --git a/intel/filterlists/decoder.go b/service/intel/filterlists/decoder.go similarity index 100% rename from intel/filterlists/decoder.go rename to service/intel/filterlists/decoder.go diff --git a/intel/filterlists/index.go b/service/intel/filterlists/index.go similarity index 99% rename from intel/filterlists/index.go rename to service/intel/filterlists/index.go index 095e3ebd..e5a593b6 100644 --- a/intel/filterlists/index.go +++ b/service/intel/filterlists/index.go @@ -12,7 +12,7 @@ import ( "github.com/safing/portbase/formats/dsd" "github.com/safing/portbase/log" "github.com/safing/portbase/updater" - "github.com/safing/portmaster/updates" + "github.com/safing/portmaster/service/updates" ) // the following definitions are copied from the intelhub repository diff --git a/intel/filterlists/keys.go b/service/intel/filterlists/keys.go similarity index 100% rename from intel/filterlists/keys.go rename to service/intel/filterlists/keys.go diff --git a/intel/filterlists/lookup.go b/service/intel/filterlists/lookup.go similarity index 100% rename from intel/filterlists/lookup.go rename to service/intel/filterlists/lookup.go diff --git a/intel/filterlists/module.go b/service/intel/filterlists/module.go similarity index 96% rename from intel/filterlists/module.go rename to service/intel/filterlists/module.go index 6f5568aa..a7846ee4 100644 --- a/intel/filterlists/module.go +++ b/service/intel/filterlists/module.go @@ -8,8 +8,8 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/modules" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/updates" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/updates" ) var module *modules.Module diff --git a/intel/filterlists/module_test.go b/service/intel/filterlists/module_test.go similarity index 100% rename from intel/filterlists/module_test.go rename to service/intel/filterlists/module_test.go diff --git a/intel/filterlists/record.go b/service/intel/filterlists/record.go similarity index 100% rename from intel/filterlists/record.go rename to service/intel/filterlists/record.go diff --git a/intel/filterlists/updater.go b/service/intel/filterlists/updater.go similarity index 100% rename from intel/filterlists/updater.go rename to service/intel/filterlists/updater.go diff --git a/intel/geoip/country_info.go b/service/intel/geoip/country_info.go similarity index 100% rename from intel/geoip/country_info.go rename to service/intel/geoip/country_info.go diff --git a/intel/geoip/country_info_test.go b/service/intel/geoip/country_info_test.go similarity index 100% rename from intel/geoip/country_info_test.go rename to service/intel/geoip/country_info_test.go diff --git a/intel/geoip/database.go b/service/intel/geoip/database.go similarity index 98% rename from intel/geoip/database.go rename to service/intel/geoip/database.go index 61bde277..57b08578 100644 --- a/intel/geoip/database.go +++ b/service/intel/geoip/database.go @@ -10,7 +10,7 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/updater" - "github.com/safing/portmaster/updates" + "github.com/safing/portmaster/service/updates" ) var worker *updateWorker diff --git a/intel/geoip/location.go b/service/intel/geoip/location.go similarity index 100% rename from intel/geoip/location.go rename to service/intel/geoip/location.go diff --git a/intel/geoip/location_test.go b/service/intel/geoip/location_test.go similarity index 100% rename from intel/geoip/location_test.go rename to service/intel/geoip/location_test.go diff --git a/intel/geoip/lookup.go b/service/intel/geoip/lookup.go similarity index 100% rename from intel/geoip/lookup.go rename to service/intel/geoip/lookup.go diff --git a/intel/geoip/lookup_test.go b/service/intel/geoip/lookup_test.go similarity index 100% rename from intel/geoip/lookup_test.go rename to service/intel/geoip/lookup_test.go diff --git a/intel/geoip/module.go b/service/intel/geoip/module.go similarity index 94% rename from intel/geoip/module.go rename to service/intel/geoip/module.go index 0c65f1af..c5d44e00 100644 --- a/intel/geoip/module.go +++ b/service/intel/geoip/module.go @@ -5,7 +5,7 @@ import ( "github.com/safing/portbase/api" "github.com/safing/portbase/modules" - "github.com/safing/portmaster/updates" + "github.com/safing/portmaster/service/updates" ) var module *modules.Module diff --git a/intel/geoip/module_test.go b/service/intel/geoip/module_test.go similarity index 64% rename from intel/geoip/module_test.go rename to service/intel/geoip/module_test.go index c1ae951b..c223d920 100644 --- a/intel/geoip/module_test.go +++ b/service/intel/geoip/module_test.go @@ -3,7 +3,7 @@ package geoip import ( "testing" - "github.com/safing/portmaster/core/pmtesting" + "github.com/safing/portmaster/service/core/pmtesting" ) func TestMain(m *testing.M) { diff --git a/intel/geoip/regions.go b/service/intel/geoip/regions.go similarity index 100% rename from intel/geoip/regions.go rename to service/intel/geoip/regions.go diff --git a/intel/geoip/regions_test.go b/service/intel/geoip/regions_test.go similarity index 100% rename from intel/geoip/regions_test.go rename to service/intel/geoip/regions_test.go diff --git a/intel/module.go b/service/intel/module.go similarity index 82% rename from intel/module.go rename to service/intel/module.go index ceec6b64..35c2d75c 100644 --- a/intel/module.go +++ b/service/intel/module.go @@ -2,7 +2,7 @@ package intel import ( "github.com/safing/portbase/modules" - _ "github.com/safing/portmaster/intel/customlists" + _ "github.com/safing/portmaster/service/intel/customlists" ) // Module of this package. Export needed for testing of the endpoints package. diff --git a/intel/resolver.go b/service/intel/resolver.go similarity index 100% rename from intel/resolver.go rename to service/intel/resolver.go diff --git a/nameserver/config.go b/service/nameserver/config.go similarity index 97% rename from nameserver/config.go rename to service/nameserver/config.go index c466a154..3e13044a 100644 --- a/nameserver/config.go +++ b/service/nameserver/config.go @@ -5,7 +5,7 @@ import ( "runtime" "github.com/safing/portbase/config" - "github.com/safing/portmaster/core" + "github.com/safing/portmaster/service/core" ) // CfgDefaultNameserverAddressKey is the config key for the listen address.. diff --git a/nameserver/conflict.go b/service/nameserver/conflict.go similarity index 94% rename from nameserver/conflict.go rename to service/nameserver/conflict.go index e02e1fd5..f716f7eb 100644 --- a/nameserver/conflict.go +++ b/service/nameserver/conflict.go @@ -7,8 +7,8 @@ import ( processInfo "github.com/shirou/gopsutil/process" "github.com/safing/portbase/log" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/network/state" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/network/state" ) var commonResolverIPs = []net.IP{ diff --git a/nameserver/failing.go b/service/nameserver/failing.go similarity index 97% rename from nameserver/failing.go rename to service/nameserver/failing.go index 1880dc96..2637a61f 100644 --- a/nameserver/failing.go +++ b/service/nameserver/failing.go @@ -4,8 +4,8 @@ import ( "sync" "time" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/resolver" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/resolver" ) type failingQuery struct { diff --git a/nameserver/metrics.go b/service/nameserver/metrics.go similarity index 100% rename from nameserver/metrics.go rename to service/nameserver/metrics.go diff --git a/nameserver/module.go b/service/nameserver/module.go similarity index 97% rename from nameserver/module.go rename to service/nameserver/module.go index ed7eb740..287ba48e 100644 --- a/nameserver/module.go +++ b/service/nameserver/module.go @@ -14,9 +14,9 @@ import ( "github.com/safing/portbase/modules" "github.com/safing/portbase/modules/subsystems" "github.com/safing/portbase/notifications" - "github.com/safing/portmaster/compat" - "github.com/safing/portmaster/firewall" - "github.com/safing/portmaster/netenv" + "github.com/safing/portmaster/service/compat" + "github.com/safing/portmaster/service/firewall" + "github.com/safing/portmaster/service/netenv" ) var ( diff --git a/nameserver/nameserver.go b/service/nameserver/nameserver.go similarity index 97% rename from nameserver/nameserver.go rename to service/nameserver/nameserver.go index 464db782..55195756 100644 --- a/nameserver/nameserver.go +++ b/service/nameserver/nameserver.go @@ -11,12 +11,12 @@ import ( "github.com/miekg/dns" "github.com/safing/portbase/log" - "github.com/safing/portmaster/firewall" - "github.com/safing/portmaster/nameserver/nsutil" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/network" - "github.com/safing/portmaster/network/netutils" - "github.com/safing/portmaster/resolver" + "github.com/safing/portmaster/service/firewall" + "github.com/safing/portmaster/service/nameserver/nsutil" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/service/resolver" ) var hostname string diff --git a/nameserver/nsutil/nsutil.go b/service/nameserver/nsutil/nsutil.go similarity index 100% rename from nameserver/nsutil/nsutil.go rename to service/nameserver/nsutil/nsutil.go diff --git a/nameserver/response.go b/service/nameserver/response.go similarity index 97% rename from nameserver/response.go rename to service/nameserver/response.go index 92dd80af..85daf140 100644 --- a/nameserver/response.go +++ b/service/nameserver/response.go @@ -7,7 +7,7 @@ import ( "github.com/miekg/dns" "github.com/safing/portbase/log" - "github.com/safing/portmaster/nameserver/nsutil" + "github.com/safing/portmaster/service/nameserver/nsutil" ) // sendResponse sends a response to query using w. The response message is diff --git a/netenv/addresses_test.go b/service/netenv/addresses_test.go similarity index 100% rename from netenv/addresses_test.go rename to service/netenv/addresses_test.go diff --git a/netenv/adresses.go b/service/netenv/adresses.go similarity index 98% rename from netenv/adresses.go rename to service/netenv/adresses.go index b050ad33..902dd0da 100644 --- a/netenv/adresses.go +++ b/service/netenv/adresses.go @@ -7,7 +7,7 @@ import ( "time" "github.com/safing/portbase/log" - "github.com/safing/portmaster/network/netutils" + "github.com/safing/portmaster/service/network/netutils" ) // GetAssignedAddresses returns the assigned IPv4 and IPv6 addresses of the host. diff --git a/netenv/api.go b/service/netenv/api.go similarity index 100% rename from netenv/api.go rename to service/netenv/api.go diff --git a/netenv/dbus_linux.go b/service/netenv/dbus_linux.go similarity index 100% rename from netenv/dbus_linux.go rename to service/netenv/dbus_linux.go diff --git a/netenv/dbus_linux_test.go b/service/netenv/dbus_linux_test.go similarity index 100% rename from netenv/dbus_linux_test.go rename to service/netenv/dbus_linux_test.go diff --git a/netenv/dialing.go b/service/netenv/dialing.go similarity index 100% rename from netenv/dialing.go rename to service/netenv/dialing.go diff --git a/netenv/environment.go b/service/netenv/environment.go similarity index 100% rename from netenv/environment.go rename to service/netenv/environment.go diff --git a/netenv/environment_default.go b/service/netenv/environment_default.go similarity index 100% rename from netenv/environment_default.go rename to service/netenv/environment_default.go diff --git a/netenv/environment_linux.go b/service/netenv/environment_linux.go similarity index 98% rename from netenv/environment_linux.go rename to service/netenv/environment_linux.go index d6b57b91..5f39875b 100644 --- a/netenv/environment_linux.go +++ b/service/netenv/environment_linux.go @@ -11,7 +11,7 @@ import ( "github.com/miekg/dns" "github.com/safing/portbase/log" - "github.com/safing/portmaster/network/netutils" + "github.com/safing/portmaster/service/network/netutils" ) var ( diff --git a/netenv/environment_linux_test.go b/service/netenv/environment_linux_test.go similarity index 100% rename from netenv/environment_linux_test.go rename to service/netenv/environment_linux_test.go diff --git a/netenv/environment_test.go b/service/netenv/environment_test.go similarity index 100% rename from netenv/environment_test.go rename to service/netenv/environment_test.go diff --git a/netenv/environment_windows.go b/service/netenv/environment_windows.go similarity index 100% rename from netenv/environment_windows.go rename to service/netenv/environment_windows.go diff --git a/netenv/environment_windows_test.go b/service/netenv/environment_windows_test.go similarity index 100% rename from netenv/environment_windows_test.go rename to service/netenv/environment_windows_test.go diff --git a/netenv/icmp_listener.go b/service/netenv/icmp_listener.go similarity index 98% rename from netenv/icmp_listener.go rename to service/netenv/icmp_listener.go index ca90b1e4..d1716d8a 100644 --- a/netenv/icmp_listener.go +++ b/service/netenv/icmp_listener.go @@ -7,7 +7,7 @@ import ( "github.com/tevino/abool" "github.com/safing/portbase/log" - "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/service/network/packet" ) /* diff --git a/netenv/location.go b/service/netenv/location.go similarity index 98% rename from netenv/location.go rename to service/netenv/location.go index 23de17ff..276e33a3 100644 --- a/netenv/location.go +++ b/service/netenv/location.go @@ -14,9 +14,9 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/rng" - "github.com/safing/portmaster/intel/geoip" - "github.com/safing/portmaster/network/netutils" - "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/service/intel/geoip" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/service/network/packet" ) var ( diff --git a/netenv/location_default.go b/service/netenv/location_default.go similarity index 100% rename from netenv/location_default.go rename to service/netenv/location_default.go diff --git a/netenv/location_test.go b/service/netenv/location_test.go similarity index 100% rename from netenv/location_test.go rename to service/netenv/location_test.go diff --git a/netenv/location_windows.go b/service/netenv/location_windows.go similarity index 100% rename from netenv/location_windows.go rename to service/netenv/location_windows.go diff --git a/netenv/main.go b/service/netenv/main.go similarity index 100% rename from netenv/main.go rename to service/netenv/main.go diff --git a/netenv/main_test.go b/service/netenv/main_test.go similarity index 65% rename from netenv/main_test.go rename to service/netenv/main_test.go index 1ee7b730..64588b38 100644 --- a/netenv/main_test.go +++ b/service/netenv/main_test.go @@ -3,7 +3,7 @@ package netenv import ( "testing" - "github.com/safing/portmaster/core/pmtesting" + "github.com/safing/portmaster/service/core/pmtesting" ) func TestMain(m *testing.M) { diff --git a/netenv/network-change.go b/service/netenv/network-change.go similarity index 100% rename from netenv/network-change.go rename to service/netenv/network-change.go diff --git a/netenv/notes.md b/service/netenv/notes.md similarity index 100% rename from netenv/notes.md rename to service/netenv/notes.md diff --git a/netenv/online-status.go b/service/netenv/online-status.go similarity index 99% rename from netenv/online-status.go rename to service/netenv/online-status.go index 7ec4a3f4..fac5e170 100644 --- a/netenv/online-status.go +++ b/service/netenv/online-status.go @@ -15,8 +15,8 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/notifications" - "github.com/safing/portmaster/network/netutils" - "github.com/safing/portmaster/updates" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/service/updates" ) // OnlineStatus represent a state of connectivity to the Internet. diff --git a/netenv/online-status_test.go b/service/netenv/online-status_test.go similarity index 100% rename from netenv/online-status_test.go rename to service/netenv/online-status_test.go diff --git a/netenv/os_android.go b/service/netenv/os_android.go similarity index 92% rename from netenv/os_android.go rename to service/netenv/os_android.go index 84c36958..aceed896 100644 --- a/netenv/os_android.go +++ b/service/netenv/os_android.go @@ -1,9 +1,10 @@ package netenv import ( - "github.com/safing/portmaster-android/go/app_interface" "net" "time" + + "github.com/safing/portmaster/service-android/go/app_interface" ) var ( diff --git a/netenv/os_default.go b/service/netenv/os_default.go similarity index 100% rename from netenv/os_default.go rename to service/netenv/os_default.go diff --git a/netquery/active_chart_handler.go b/service/netquery/active_chart_handler.go similarity index 98% rename from netquery/active_chart_handler.go rename to service/netquery/active_chart_handler.go index 08628394..2d2fb682 100644 --- a/netquery/active_chart_handler.go +++ b/service/netquery/active_chart_handler.go @@ -10,7 +10,7 @@ import ( "net/http" "strings" - "github.com/safing/portmaster/netquery/orm" + "github.com/safing/portmaster/service/netquery/orm" ) // ActiveChartHandler handles requests for connection charts. diff --git a/netquery/bandwidth_chart_handler.go b/service/netquery/bandwidth_chart_handler.go similarity index 98% rename from netquery/bandwidth_chart_handler.go rename to service/netquery/bandwidth_chart_handler.go index 5bb5b526..615682e6 100644 --- a/netquery/bandwidth_chart_handler.go +++ b/service/netquery/bandwidth_chart_handler.go @@ -10,7 +10,7 @@ import ( "net/http" "strings" - "github.com/safing/portmaster/netquery/orm" + "github.com/safing/portmaster/service/netquery/orm" ) // BandwidthChartHandler handles requests for connection charts. diff --git a/netquery/database.go b/service/netquery/database.go similarity index 98% rename from netquery/database.go rename to service/netquery/database.go index e3287345..cb9f4039 100644 --- a/netquery/database.go +++ b/service/netquery/database.go @@ -19,11 +19,11 @@ import ( "github.com/safing/portbase/config" "github.com/safing/portbase/dataroot" "github.com/safing/portbase/log" - "github.com/safing/portmaster/netquery/orm" - "github.com/safing/portmaster/network" - "github.com/safing/portmaster/network/netutils" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/profile" + "github.com/safing/portmaster/service/netquery/orm" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/profile" ) // InMemory is the "file path" to open a new in-memory database. diff --git a/netquery/manager.go b/service/netquery/manager.go similarity index 99% rename from netquery/manager.go rename to service/netquery/manager.go index 34b779b5..76403e03 100644 --- a/netquery/manager.go +++ b/service/netquery/manager.go @@ -10,7 +10,7 @@ import ( "github.com/safing/portbase/formats/dsd" "github.com/safing/portbase/log" "github.com/safing/portbase/runtime" - "github.com/safing/portmaster/network" + "github.com/safing/portmaster/service/network" ) type ( diff --git a/netquery/module_api.go b/service/netquery/module_api.go similarity index 99% rename from netquery/module_api.go rename to service/netquery/module_api.go index b4e56b02..00950a01 100644 --- a/netquery/module_api.go +++ b/service/netquery/module_api.go @@ -17,7 +17,7 @@ import ( "github.com/safing/portbase/modules" "github.com/safing/portbase/modules/subsystems" "github.com/safing/portbase/runtime" - "github.com/safing/portmaster/network" + "github.com/safing/portmaster/service/network" ) // DefaultModule is the default netquery module. diff --git a/netquery/orm/decoder.go b/service/netquery/orm/decoder.go similarity index 100% rename from netquery/orm/decoder.go rename to service/netquery/orm/decoder.go diff --git a/netquery/orm/decoder_test.go b/service/netquery/orm/decoder_test.go similarity index 100% rename from netquery/orm/decoder_test.go rename to service/netquery/orm/decoder_test.go diff --git a/netquery/orm/encoder.go b/service/netquery/orm/encoder.go similarity index 100% rename from netquery/orm/encoder.go rename to service/netquery/orm/encoder.go diff --git a/netquery/orm/encoder_test.go b/service/netquery/orm/encoder_test.go similarity index 100% rename from netquery/orm/encoder_test.go rename to service/netquery/orm/encoder_test.go diff --git a/netquery/orm/query_runner.go b/service/netquery/orm/query_runner.go similarity index 100% rename from netquery/orm/query_runner.go rename to service/netquery/orm/query_runner.go diff --git a/netquery/orm/schema_builder.go b/service/netquery/orm/schema_builder.go similarity index 100% rename from netquery/orm/schema_builder.go rename to service/netquery/orm/schema_builder.go diff --git a/netquery/orm/schema_builder_test.go b/service/netquery/orm/schema_builder_test.go similarity index 100% rename from netquery/orm/schema_builder_test.go rename to service/netquery/orm/schema_builder_test.go diff --git a/netquery/query.go b/service/netquery/query.go similarity index 99% rename from netquery/query.go rename to service/netquery/query.go index 2b81bfb1..cb84ac30 100644 --- a/netquery/query.go +++ b/service/netquery/query.go @@ -13,7 +13,7 @@ import ( "golang.org/x/exp/slices" "zombiezen.com/go/sqlite" - "github.com/safing/portmaster/netquery/orm" + "github.com/safing/portmaster/service/netquery/orm" ) // DatabaseName is a database name constant. diff --git a/netquery/query_handler.go b/service/netquery/query_handler.go similarity index 99% rename from netquery/query_handler.go rename to service/netquery/query_handler.go index 8e704d3e..68b1feb2 100644 --- a/netquery/query_handler.go +++ b/service/netquery/query_handler.go @@ -14,7 +14,7 @@ import ( servertiming "github.com/mitchellh/go-server-timing" "github.com/safing/portbase/log" - "github.com/safing/portmaster/netquery/orm" + "github.com/safing/portmaster/service/netquery/orm" ) var charOnlyRegexp = regexp.MustCompile("[a-zA-Z]+") diff --git a/netquery/query_request.go b/service/netquery/query_request.go similarity index 99% rename from netquery/query_request.go rename to service/netquery/query_request.go index ea5162a9..97fc5789 100644 --- a/netquery/query_request.go +++ b/service/netquery/query_request.go @@ -7,7 +7,7 @@ import ( "golang.org/x/exp/slices" - "github.com/safing/portmaster/netquery/orm" + "github.com/safing/portmaster/service/netquery/orm" ) type ( diff --git a/netquery/query_test.go b/service/netquery/query_test.go similarity index 98% rename from netquery/query_test.go rename to service/netquery/query_test.go index afd65b4f..bc9fde27 100644 --- a/netquery/query_test.go +++ b/service/netquery/query_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/safing/portmaster/netquery/orm" + "github.com/safing/portmaster/service/netquery/orm" ) func TestUnmarshalQuery(t *testing.T) { //nolint:tparallel diff --git a/netquery/runtime_query_runner.go b/service/netquery/runtime_query_runner.go similarity index 97% rename from netquery/runtime_query_runner.go rename to service/netquery/runtime_query_runner.go index 3b443ec5..67ba449b 100644 --- a/netquery/runtime_query_runner.go +++ b/service/netquery/runtime_query_runner.go @@ -10,7 +10,7 @@ import ( "github.com/safing/portbase/formats/dsd" "github.com/safing/portbase/log" "github.com/safing/portbase/runtime" - "github.com/safing/portmaster/netquery/orm" + "github.com/safing/portmaster/service/netquery/orm" ) // RuntimeQueryRunner provides a simple interface for the runtime database diff --git a/network/api.go b/service/network/api.go similarity index 97% rename from network/api.go rename to service/network/api.go index c59b5aaf..afb2d610 100644 --- a/network/api.go +++ b/service/network/api.go @@ -12,11 +12,11 @@ import ( "github.com/safing/portbase/config" "github.com/safing/portbase/database/query" "github.com/safing/portbase/utils/debug" - "github.com/safing/portmaster/network/state" - "github.com/safing/portmaster/process" - "github.com/safing/portmaster/resolver" - "github.com/safing/portmaster/status" - "github.com/safing/portmaster/updates" + "github.com/safing/portmaster/service/network/state" + "github.com/safing/portmaster/service/process" + "github.com/safing/portmaster/service/resolver" + "github.com/safing/portmaster/service/status" + "github.com/safing/portmaster/service/updates" ) func registerAPIEndpoints() error { diff --git a/network/api_test.go b/service/network/api_test.go similarity index 98% rename from network/api_test.go rename to service/network/api_test.go index 62647527..c44109b0 100644 --- a/network/api_test.go +++ b/service/network/api_test.go @@ -5,7 +5,7 @@ import ( "net" "testing" - "github.com/safing/portmaster/intel" + "github.com/safing/portmaster/service/intel" ) func TestDebugInfoLineFormatting(t *testing.T) { diff --git a/network/clean.go b/service/network/clean.go similarity index 95% rename from network/clean.go rename to service/network/clean.go index b15fbaa0..9901b00b 100644 --- a/network/clean.go +++ b/service/network/clean.go @@ -5,9 +5,9 @@ import ( "time" "github.com/safing/portbase/log" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/network/state" - "github.com/safing/portmaster/process" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/network/state" + "github.com/safing/portmaster/service/process" ) const ( diff --git a/network/connection.go b/service/network/connection.go similarity index 98% rename from network/connection.go rename to service/network/connection.go index b1ed96fe..32ba8ee9 100644 --- a/network/connection.go +++ b/service/network/connection.go @@ -13,16 +13,16 @@ import ( "github.com/safing/portbase/database/record" "github.com/safing/portbase/log" "github.com/safing/portbase/notifications" - "github.com/safing/portmaster/intel" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/network/netutils" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/process" - _ "github.com/safing/portmaster/process/tags" - "github.com/safing/portmaster/resolver" - "github.com/safing/spn/access" - "github.com/safing/spn/access/account" - "github.com/safing/spn/navigator" + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/process" + _ "github.com/safing/portmaster/service/process/tags" + "github.com/safing/portmaster/service/resolver" + "github.com/safing/portmaster/spn/access" + "github.com/safing/portmaster/spn/access/account" + "github.com/safing/portmaster/spn/navigator" ) // FirewallHandler defines the function signature for a firewall diff --git a/network/connection_android.go b/service/network/connection_android.go similarity index 88% rename from network/connection_android.go rename to service/network/connection_android.go index 71b16ed4..bbd49864 100644 --- a/network/connection_android.go +++ b/service/network/connection_android.go @@ -6,11 +6,11 @@ import ( "net" "time" - "github.com/safing/portmaster/intel" - "github.com/safing/portmaster/network/netutils" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/process" - "github.com/safing/spn/navigator" + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/process" + "github.com/safing/portmaster/spn/navigator" "github.com/tevino/abool" ) diff --git a/network/connection_store.go b/service/network/connection_store.go similarity index 100% rename from network/connection_store.go rename to service/network/connection_store.go diff --git a/network/database.go b/service/network/database.go similarity index 98% rename from network/database.go rename to service/network/database.go index 457b2693..9b098d48 100644 --- a/network/database.go +++ b/service/network/database.go @@ -11,7 +11,7 @@ import ( "github.com/safing/portbase/database/query" "github.com/safing/portbase/database/record" "github.com/safing/portbase/database/storage" - "github.com/safing/portmaster/process" + "github.com/safing/portmaster/service/process" ) const ( diff --git a/network/dns.go b/service/network/dns.go similarity index 97% rename from network/dns.go rename to service/network/dns.go index a0bef466..201dd25b 100644 --- a/network/dns.go +++ b/service/network/dns.go @@ -11,10 +11,10 @@ import ( "golang.org/x/exp/slices" "github.com/safing/portbase/log" - "github.com/safing/portmaster/nameserver/nsutil" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/process" - "github.com/safing/portmaster/resolver" + "github.com/safing/portmaster/service/nameserver/nsutil" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/process" + "github.com/safing/portmaster/service/resolver" ) var ( diff --git a/network/iphelper/get.go b/service/network/iphelper/get.go similarity index 96% rename from network/iphelper/get.go rename to service/network/iphelper/get.go index 31f1c925..e78c70fc 100644 --- a/network/iphelper/get.go +++ b/service/network/iphelper/get.go @@ -5,7 +5,7 @@ package iphelper import ( "sync" - "github.com/safing/portmaster/network/socket" + "github.com/safing/portmaster/service/network/socket" ) var ( diff --git a/network/iphelper/iphelper.go b/service/network/iphelper/iphelper.go similarity index 100% rename from network/iphelper/iphelper.go rename to service/network/iphelper/iphelper.go diff --git a/network/iphelper/tables.go b/service/network/iphelper/tables.go similarity index 99% rename from network/iphelper/tables.go rename to service/network/iphelper/tables.go index 94998d7e..9e082173 100644 --- a/network/iphelper/tables.go +++ b/service/network/iphelper/tables.go @@ -11,7 +11,7 @@ import ( "unsafe" "github.com/safing/portbase/log" - "github.com/safing/portmaster/network/socket" + "github.com/safing/portmaster/service/network/socket" "golang.org/x/sys/windows" ) diff --git a/network/iphelper/tables_test.go b/service/network/iphelper/tables_test.go similarity index 100% rename from network/iphelper/tables_test.go rename to service/network/iphelper/tables_test.go diff --git a/network/metrics.go b/service/network/metrics.go similarity index 98% rename from network/metrics.go rename to service/network/metrics.go index 66f19e1b..5ffa1880 100644 --- a/network/metrics.go +++ b/service/network/metrics.go @@ -4,7 +4,7 @@ import ( "github.com/safing/portbase/api" "github.com/safing/portbase/config" "github.com/safing/portbase/metrics" - "github.com/safing/portmaster/process" + "github.com/safing/portmaster/service/process" ) var ( diff --git a/network/module.go b/service/network/module.go similarity index 96% rename from network/module.go rename to service/network/module.go index 1a7fe891..bebcb467 100644 --- a/network/module.go +++ b/service/network/module.go @@ -8,9 +8,9 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/modules" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/network/state" - "github.com/safing/portmaster/profile" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network/state" + "github.com/safing/portmaster/service/profile" ) var ( diff --git a/network/multicast.go b/service/network/multicast.go similarity index 96% rename from network/multicast.go rename to service/network/multicast.go index d7c8f9a7..d12809a9 100644 --- a/network/multicast.go +++ b/service/network/multicast.go @@ -3,7 +3,7 @@ package network import ( "net" - "github.com/safing/portmaster/network/netutils" + "github.com/safing/portmaster/service/network/netutils" ) // GetMulticastRequestConn searches for and returns the requesting connnection diff --git a/network/netutils/address.go b/service/network/netutils/address.go similarity index 96% rename from network/netutils/address.go rename to service/network/netutils/address.go index 3d89c39c..44337392 100644 --- a/network/netutils/address.go +++ b/service/network/netutils/address.go @@ -5,7 +5,7 @@ import ( "net" "strconv" - "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/service/network/packet" ) var errInvalidIP = errors.New("invalid IP address") diff --git a/network/netutils/dns.go b/service/network/netutils/dns.go similarity index 100% rename from network/netutils/dns.go rename to service/network/netutils/dns.go diff --git a/network/netutils/dns_test.go b/service/network/netutils/dns_test.go similarity index 100% rename from network/netutils/dns_test.go rename to service/network/netutils/dns_test.go diff --git a/network/netutils/ip.go b/service/network/netutils/ip.go similarity index 100% rename from network/netutils/ip.go rename to service/network/netutils/ip.go diff --git a/network/netutils/ip_test.go b/service/network/netutils/ip_test.go similarity index 100% rename from network/netutils/ip_test.go rename to service/network/netutils/ip_test.go diff --git a/network/netutils/tcpassembly.go b/service/network/netutils/tcpassembly.go similarity index 100% rename from network/netutils/tcpassembly.go rename to service/network/netutils/tcpassembly.go diff --git a/network/packet/bandwidth.go b/service/network/packet/bandwidth.go similarity index 100% rename from network/packet/bandwidth.go rename to service/network/packet/bandwidth.go diff --git a/network/packet/const.go b/service/network/packet/const.go similarity index 100% rename from network/packet/const.go rename to service/network/packet/const.go diff --git a/network/packet/info_only.go b/service/network/packet/info_only.go similarity index 100% rename from network/packet/info_only.go rename to service/network/packet/info_only.go diff --git a/network/packet/packet.go b/service/network/packet/packet.go similarity index 100% rename from network/packet/packet.go rename to service/network/packet/packet.go diff --git a/network/packet/packetinfo.go b/service/network/packet/packetinfo.go similarity index 100% rename from network/packet/packetinfo.go rename to service/network/packet/packetinfo.go diff --git a/network/packet/parse.go b/service/network/packet/parse.go similarity index 100% rename from network/packet/parse.go rename to service/network/packet/parse.go diff --git a/network/ports.go b/service/network/ports.go similarity index 100% rename from network/ports.go rename to service/network/ports.go diff --git a/network/proc/findpid.go b/service/network/proc/findpid.go similarity index 97% rename from network/proc/findpid.go rename to service/network/proc/findpid.go index 2fbb7130..e5cd5185 100644 --- a/network/proc/findpid.go +++ b/service/network/proc/findpid.go @@ -9,7 +9,7 @@ import ( "strconv" "github.com/safing/portbase/log" - "github.com/safing/portmaster/network/socket" + "github.com/safing/portmaster/service/network/socket" ) // GetPID returns the already existing pid of the given socket info or searches for it. diff --git a/network/proc/pids_by_user.go b/service/network/proc/pids_by_user.go similarity index 100% rename from network/proc/pids_by_user.go rename to service/network/proc/pids_by_user.go diff --git a/network/proc/tables.go b/service/network/proc/tables.go similarity index 99% rename from network/proc/tables.go rename to service/network/proc/tables.go index 62c4a4c5..2569a7f0 100644 --- a/network/proc/tables.go +++ b/service/network/proc/tables.go @@ -13,7 +13,7 @@ import ( "unicode" "github.com/safing/portbase/log" - "github.com/safing/portmaster/network/socket" + "github.com/safing/portmaster/service/network/socket" ) /* diff --git a/network/proc/tables_test.go b/service/network/proc/tables_test.go similarity index 100% rename from network/proc/tables_test.go rename to service/network/proc/tables_test.go diff --git a/network/reference/ports.go b/service/network/reference/ports.go similarity index 100% rename from network/reference/ports.go rename to service/network/reference/ports.go diff --git a/network/reference/protocols.go b/service/network/reference/protocols.go similarity index 100% rename from network/reference/protocols.go rename to service/network/reference/protocols.go diff --git a/network/socket/socket.go b/service/network/socket/socket.go similarity index 100% rename from network/socket/socket.go rename to service/network/socket/socket.go diff --git a/network/state/exists.go b/service/network/state/exists.go similarity index 95% rename from network/state/exists.go rename to service/network/state/exists.go index ed0c48c3..cbe81239 100644 --- a/network/state/exists.go +++ b/service/network/state/exists.go @@ -3,8 +3,8 @@ package state import ( "time" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/network/socket" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/network/socket" ) const ( diff --git a/network/state/info.go b/service/network/state/info.go similarity index 89% rename from network/state/info.go rename to service/network/state/info.go index 483cd66e..306c36a0 100644 --- a/network/state/info.go +++ b/service/network/state/info.go @@ -4,8 +4,8 @@ import ( "sync" "github.com/safing/portbase/database/record" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/network/socket" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network/socket" ) // Info holds network state information as provided by the system. diff --git a/network/state/lookup.go b/service/network/state/lookup.go similarity index 97% rename from network/state/lookup.go rename to service/network/state/lookup.go index 35006b2c..39f3d2d9 100644 --- a/network/state/lookup.go +++ b/service/network/state/lookup.go @@ -3,9 +3,9 @@ package state import ( "errors" - "github.com/safing/portmaster/network/netutils" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/network/socket" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/network/socket" ) // - TCP diff --git a/network/state/system_default.go b/service/network/state/system_default.go similarity index 95% rename from network/state/system_default.go rename to service/network/state/system_default.go index 4b798996..9ccf96c9 100644 --- a/network/state/system_default.go +++ b/service/network/state/system_default.go @@ -7,7 +7,7 @@ import ( "time" "github.com/safing/portbase/config" - "github.com/safing/portmaster/network/socket" + "github.com/safing/portmaster/service/network/socket" ) func init() { diff --git a/network/state/system_linux.go b/service/network/state/system_linux.go similarity index 90% rename from network/state/system_linux.go rename to service/network/state/system_linux.go index c3e792a8..6c6bfe6f 100644 --- a/network/state/system_linux.go +++ b/service/network/state/system_linux.go @@ -3,8 +3,8 @@ package state import ( "time" - "github.com/safing/portmaster/network/proc" - "github.com/safing/portmaster/network/socket" + "github.com/safing/portmaster/service/network/proc" + "github.com/safing/portmaster/service/network/socket" ) var ( diff --git a/network/state/system_windows.go b/service/network/state/system_windows.go similarity index 80% rename from network/state/system_windows.go rename to service/network/state/system_windows.go index 2a95a01e..fea998dd 100644 --- a/network/state/system_windows.go +++ b/service/network/state/system_windows.go @@ -1,8 +1,8 @@ package state import ( - "github.com/safing/portmaster/network/iphelper" - "github.com/safing/portmaster/network/socket" + "github.com/safing/portmaster/service/network/iphelper" + "github.com/safing/portmaster/service/network/socket" ) var ( diff --git a/network/state/tcp.go b/service/network/state/tcp.go similarity index 97% rename from network/state/tcp.go rename to service/network/state/tcp.go index 5f8c03d7..33e053be 100644 --- a/network/state/tcp.go +++ b/service/network/state/tcp.go @@ -8,7 +8,7 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/utils" - "github.com/safing/portmaster/network/socket" + "github.com/safing/portmaster/service/network/socket" ) const ( diff --git a/network/state/udp.go b/service/network/state/udp.go similarity index 97% rename from network/state/udp.go rename to service/network/state/udp.go index 40696820..ce7139e4 100644 --- a/network/state/udp.go +++ b/service/network/state/udp.go @@ -10,9 +10,9 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/utils" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/network/socket" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/network/socket" ) type udpTable struct { diff --git a/network/status.go b/service/network/status.go similarity index 100% rename from network/status.go rename to service/network/status.go diff --git a/process/api.go b/service/process/api.go similarity index 98% rename from process/api.go rename to service/process/api.go index 0f5c43a4..b687ae83 100644 --- a/process/api.go +++ b/service/process/api.go @@ -7,7 +7,7 @@ import ( "strconv" "github.com/safing/portbase/api" - "github.com/safing/portmaster/profile" + "github.com/safing/portmaster/service/profile" ) func registerAPIEndpoints() error { diff --git a/process/config.go b/service/process/config.go similarity index 100% rename from process/config.go rename to service/process/config.go diff --git a/process/database.go b/service/process/database.go similarity index 98% rename from process/database.go rename to service/process/database.go index 2041c6cf..82a6dcb8 100644 --- a/process/database.go +++ b/service/process/database.go @@ -13,7 +13,7 @@ import ( "github.com/safing/portbase/database" "github.com/safing/portbase/log" - "github.com/safing/portmaster/profile" + "github.com/safing/portmaster/service/profile" ) const processDatabaseNamespace = "network:tree" diff --git a/process/doc.go b/service/process/doc.go similarity index 100% rename from process/doc.go rename to service/process/doc.go diff --git a/process/executable.go b/service/process/executable.go similarity index 100% rename from process/executable.go rename to service/process/executable.go diff --git a/process/find.go b/service/process/find.go similarity index 95% rename from process/find.go rename to service/process/find.go index be5afdb6..98681832 100644 --- a/process/find.go +++ b/service/process/find.go @@ -8,10 +8,10 @@ import ( "github.com/safing/portbase/api" "github.com/safing/portbase/log" - "github.com/safing/portmaster/network/netutils" - "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/network/state" - "github.com/safing/portmaster/profile" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/service/network/state" + "github.com/safing/portmaster/service/profile" ) // GetProcessWithProfile returns the process, including the profile. diff --git a/process/module.go b/service/process/module.go similarity index 91% rename from process/module.go rename to service/process/module.go index b33be8ca..cef4fe2a 100644 --- a/process/module.go +++ b/service/process/module.go @@ -4,7 +4,7 @@ import ( "os" "github.com/safing/portbase/modules" - "github.com/safing/portmaster/updates" + "github.com/safing/portmaster/service/updates" ) var ( diff --git a/process/module_test.go b/service/process/module_test.go similarity index 65% rename from process/module_test.go rename to service/process/module_test.go index fc33c7bd..f2350d94 100644 --- a/process/module_test.go +++ b/service/process/module_test.go @@ -3,7 +3,7 @@ package process import ( "testing" - "github.com/safing/portmaster/core/pmtesting" + "github.com/safing/portmaster/service/core/pmtesting" ) func TestMain(m *testing.M) { diff --git a/process/process.go b/service/process/process.go similarity index 99% rename from process/process.go rename to service/process/process.go index b7d0cf41..4508310e 100644 --- a/process/process.go +++ b/service/process/process.go @@ -15,7 +15,7 @@ import ( "github.com/safing/portbase/database/record" "github.com/safing/portbase/log" - "github.com/safing/portmaster/profile" + "github.com/safing/portmaster/service/profile" ) const onLinux = runtime.GOOS == "linux" diff --git a/process/process_default.go b/service/process/process_default.go similarity index 100% rename from process/process_default.go rename to service/process/process_default.go diff --git a/process/process_linux.go b/service/process/process_linux.go similarity index 100% rename from process/process_linux.go rename to service/process/process_linux.go diff --git a/process/process_windows.go b/service/process/process_windows.go similarity index 100% rename from process/process_windows.go rename to service/process/process_windows.go diff --git a/process/profile.go b/service/process/profile.go similarity index 98% rename from process/profile.go rename to service/process/profile.go index 27c0f985..53599913 100644 --- a/process/profile.go +++ b/service/process/profile.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/safing/portbase/log" - "github.com/safing/portmaster/profile" + "github.com/safing/portmaster/service/profile" ) var ownPID = os.Getpid() diff --git a/process/special.go b/service/process/special.go similarity index 96% rename from process/special.go rename to service/process/special.go index aa35160a..5733c2ba 100644 --- a/process/special.go +++ b/service/process/special.go @@ -7,8 +7,8 @@ import ( "golang.org/x/sync/singleflight" "github.com/safing/portbase/log" - "github.com/safing/portmaster/network/socket" - "github.com/safing/portmaster/profile" + "github.com/safing/portmaster/service/network/socket" + "github.com/safing/portmaster/service/profile" ) const ( diff --git a/process/tags.go b/service/process/tags.go similarity index 97% rename from process/tags.go rename to service/process/tags.go index 0eea7f49..dd8a43c5 100644 --- a/process/tags.go +++ b/service/process/tags.go @@ -4,7 +4,7 @@ import ( "errors" "sync" - "github.com/safing/portmaster/profile" + "github.com/safing/portmaster/service/profile" ) var ( diff --git a/process/tags/appimage_unix.go b/service/process/tags/appimage_unix.go similarity index 96% rename from process/tags/appimage_unix.go rename to service/process/tags/appimage_unix.go index 17cbaba2..1e1bd259 100644 --- a/process/tags/appimage_unix.go +++ b/service/process/tags/appimage_unix.go @@ -8,9 +8,9 @@ import ( "strings" "github.com/safing/portbase/log" - "github.com/safing/portmaster/process" - "github.com/safing/portmaster/profile" - "github.com/safing/portmaster/profile/binmeta" + "github.com/safing/portmaster/service/process" + "github.com/safing/portmaster/service/profile" + "github.com/safing/portmaster/service/profile/binmeta" ) func init() { diff --git a/process/tags/flatpak_unix.go b/service/process/tags/flatpak_unix.go similarity index 92% rename from process/tags/flatpak_unix.go rename to service/process/tags/flatpak_unix.go index 78eafe53..ea9e9c5a 100644 --- a/process/tags/flatpak_unix.go +++ b/service/process/tags/flatpak_unix.go @@ -3,9 +3,9 @@ package tags import ( "strings" - "github.com/safing/portmaster/process" - "github.com/safing/portmaster/profile" - "github.com/safing/portmaster/profile/binmeta" + "github.com/safing/portmaster/service/process" + "github.com/safing/portmaster/service/profile" + "github.com/safing/portmaster/service/profile/binmeta" ) func init() { diff --git a/process/tags/interpreter_unix.go b/service/process/tags/interpreter_unix.go similarity index 97% rename from process/tags/interpreter_unix.go rename to service/process/tags/interpreter_unix.go index 7e9dfdfc..7e5c28b9 100644 --- a/process/tags/interpreter_unix.go +++ b/service/process/tags/interpreter_unix.go @@ -12,9 +12,9 @@ import ( "github.com/google/shlex" - "github.com/safing/portmaster/process" - "github.com/safing/portmaster/profile" - "github.com/safing/portmaster/profile/binmeta" + "github.com/safing/portmaster/service/process" + "github.com/safing/portmaster/service/profile" + "github.com/safing/portmaster/service/profile/binmeta" ) func init() { diff --git a/process/tags/net.go b/service/process/tags/net.go similarity index 93% rename from process/tags/net.go rename to service/process/tags/net.go index 8c6196e5..ce608513 100644 --- a/process/tags/net.go +++ b/service/process/tags/net.go @@ -1,8 +1,8 @@ package tags import ( - "github.com/safing/portmaster/process" - "github.com/safing/portmaster/profile" + "github.com/safing/portmaster/service/process" + "github.com/safing/portmaster/service/profile" ) func init() { diff --git a/process/tags/snap_unix.go b/service/process/tags/snap_unix.go similarity index 95% rename from process/tags/snap_unix.go rename to service/process/tags/snap_unix.go index 70e65299..667ac485 100644 --- a/process/tags/snap_unix.go +++ b/service/process/tags/snap_unix.go @@ -3,9 +3,9 @@ package tags import ( "strings" - "github.com/safing/portmaster/process" - "github.com/safing/portmaster/profile" - "github.com/safing/portmaster/profile/binmeta" + "github.com/safing/portmaster/service/process" + "github.com/safing/portmaster/service/profile" + "github.com/safing/portmaster/service/profile/binmeta" ) func init() { diff --git a/process/tags/svchost_windows.go b/service/process/tags/svchost_windows.go similarity index 95% rename from process/tags/svchost_windows.go rename to service/process/tags/svchost_windows.go index 44071228..83087cbc 100644 --- a/process/tags/svchost_windows.go +++ b/service/process/tags/svchost_windows.go @@ -7,9 +7,9 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/utils/osdetail" - "github.com/safing/portmaster/process" - "github.com/safing/portmaster/profile" - "github.com/safing/portmaster/profile/binmeta" + "github.com/safing/portmaster/service/process" + "github.com/safing/portmaster/service/profile" + "github.com/safing/portmaster/service/profile/binmeta" ) func init() { diff --git a/process/tags/winstore_windows.go b/service/process/tags/winstore_windows.go similarity index 95% rename from process/tags/winstore_windows.go rename to service/process/tags/winstore_windows.go index 0948be97..e41995c8 100644 --- a/process/tags/winstore_windows.go +++ b/service/process/tags/winstore_windows.go @@ -6,9 +6,9 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/utils" - "github.com/safing/portmaster/process" - "github.com/safing/portmaster/profile" - "github.com/safing/portmaster/profile/binmeta" + "github.com/safing/portmaster/service/process" + "github.com/safing/portmaster/service/profile" + "github.com/safing/portmaster/service/profile/binmeta" ) func init() { diff --git a/profile/active.go b/service/profile/active.go similarity index 100% rename from profile/active.go rename to service/profile/active.go diff --git a/profile/api.go b/service/profile/api.go similarity index 98% rename from profile/api.go rename to service/profile/api.go index ca43031e..7b02e914 100644 --- a/profile/api.go +++ b/service/profile/api.go @@ -10,7 +10,7 @@ import ( "github.com/safing/portbase/api" "github.com/safing/portbase/formats/dsd" "github.com/safing/portbase/utils" - "github.com/safing/portmaster/profile/binmeta" + "github.com/safing/portmaster/service/profile/binmeta" ) func registerAPIEndpoints() error { diff --git a/profile/binmeta/convert.go b/service/profile/binmeta/convert.go similarity index 100% rename from profile/binmeta/convert.go rename to service/profile/binmeta/convert.go diff --git a/profile/binmeta/find_default.go b/service/profile/binmeta/find_default.go similarity index 100% rename from profile/binmeta/find_default.go rename to service/profile/binmeta/find_default.go diff --git a/profile/binmeta/find_linux.go b/service/profile/binmeta/find_linux.go similarity index 100% rename from profile/binmeta/find_linux.go rename to service/profile/binmeta/find_linux.go diff --git a/profile/binmeta/find_linux_test.go b/service/profile/binmeta/find_linux_test.go similarity index 100% rename from profile/binmeta/find_linux_test.go rename to service/profile/binmeta/find_linux_test.go diff --git a/profile/binmeta/find_windows.go b/service/profile/binmeta/find_windows.go similarity index 100% rename from profile/binmeta/find_windows.go rename to service/profile/binmeta/find_windows.go diff --git a/profile/binmeta/find_windows_test.go b/service/profile/binmeta/find_windows_test.go similarity index 100% rename from profile/binmeta/find_windows_test.go rename to service/profile/binmeta/find_windows_test.go diff --git a/profile/binmeta/icon.go b/service/profile/binmeta/icon.go similarity index 100% rename from profile/binmeta/icon.go rename to service/profile/binmeta/icon.go diff --git a/profile/binmeta/icons.go b/service/profile/binmeta/icons.go similarity index 100% rename from profile/binmeta/icons.go rename to service/profile/binmeta/icons.go diff --git a/profile/binmeta/locations_linux.go b/service/profile/binmeta/locations_linux.go similarity index 100% rename from profile/binmeta/locations_linux.go rename to service/profile/binmeta/locations_linux.go diff --git a/profile/binmeta/name.go b/service/profile/binmeta/name.go similarity index 100% rename from profile/binmeta/name.go rename to service/profile/binmeta/name.go diff --git a/profile/binmeta/name_test.go b/service/profile/binmeta/name_test.go similarity index 100% rename from profile/binmeta/name_test.go rename to service/profile/binmeta/name_test.go diff --git a/profile/config-update.go b/service/profile/config-update.go similarity index 96% rename from profile/config-update.go rename to service/profile/config-update.go index 3a6cd246..3c31603c 100644 --- a/profile/config-update.go +++ b/service/profile/config-update.go @@ -7,8 +7,8 @@ import ( "time" "github.com/safing/portbase/modules" - "github.com/safing/portmaster/intel/filterlists" - "github.com/safing/portmaster/profile/endpoints" + "github.com/safing/portmaster/service/intel/filterlists" + "github.com/safing/portmaster/service/profile/endpoints" ) var ( diff --git a/profile/config.go b/service/profile/config.go similarity index 99% rename from profile/config.go rename to service/profile/config.go index 18495ae9..a2b5da0a 100644 --- a/profile/config.go +++ b/service/profile/config.go @@ -4,9 +4,9 @@ import ( "strings" "github.com/safing/portbase/config" - "github.com/safing/portmaster/profile/endpoints" - "github.com/safing/portmaster/status" - "github.com/safing/spn/access/account" + "github.com/safing/portmaster/service/profile/endpoints" + "github.com/safing/portmaster/service/status" + "github.com/safing/portmaster/spn/access/account" ) // Configuration Keys. diff --git a/profile/database.go b/service/profile/database.go similarity index 100% rename from profile/database.go rename to service/profile/database.go diff --git a/profile/endpoints/annotations.go b/service/profile/endpoints/annotations.go similarity index 100% rename from profile/endpoints/annotations.go rename to service/profile/endpoints/annotations.go diff --git a/profile/endpoints/endpoint-any.go b/service/profile/endpoints/endpoint-any.go similarity index 92% rename from profile/endpoints/endpoint-any.go rename to service/profile/endpoints/endpoint-any.go index 14960489..7ec64688 100644 --- a/profile/endpoints/endpoint-any.go +++ b/service/profile/endpoints/endpoint-any.go @@ -3,7 +3,7 @@ package endpoints import ( "context" - "github.com/safing/portmaster/intel" + "github.com/safing/portmaster/service/intel" ) // EndpointAny matches anything. diff --git a/profile/endpoints/endpoint-asn.go b/service/profile/endpoints/endpoint-asn.go similarity index 96% rename from profile/endpoints/endpoint-asn.go rename to service/profile/endpoints/endpoint-asn.go index 5341f81b..20864d72 100644 --- a/profile/endpoints/endpoint-asn.go +++ b/service/profile/endpoints/endpoint-asn.go @@ -7,7 +7,7 @@ import ( "strconv" "strings" - "github.com/safing/portmaster/intel" + "github.com/safing/portmaster/service/intel" ) var asnRegex = regexp.MustCompile("^AS[0-9]+$") diff --git a/profile/endpoints/endpoint-continent.go b/service/profile/endpoints/endpoint-continent.go similarity index 96% rename from profile/endpoints/endpoint-continent.go rename to service/profile/endpoints/endpoint-continent.go index f241cfa2..4ba244da 100644 --- a/profile/endpoints/endpoint-continent.go +++ b/service/profile/endpoints/endpoint-continent.go @@ -6,7 +6,7 @@ import ( "regexp" "strings" - "github.com/safing/portmaster/intel" + "github.com/safing/portmaster/service/intel" ) var ( diff --git a/profile/endpoints/endpoint-country.go b/service/profile/endpoints/endpoint-country.go similarity index 96% rename from profile/endpoints/endpoint-country.go rename to service/profile/endpoints/endpoint-country.go index c8e1f6df..60a478cf 100644 --- a/profile/endpoints/endpoint-country.go +++ b/service/profile/endpoints/endpoint-country.go @@ -6,7 +6,7 @@ import ( "regexp" "strings" - "github.com/safing/portmaster/intel" + "github.com/safing/portmaster/service/intel" ) var countryRegex = regexp.MustCompile(`^[A-Z]{2}$`) diff --git a/profile/endpoints/endpoint-domain.go b/service/profile/endpoints/endpoint-domain.go similarity index 97% rename from profile/endpoints/endpoint-domain.go rename to service/profile/endpoints/endpoint-domain.go index d82ccb5b..cdb6f248 100644 --- a/profile/endpoints/endpoint-domain.go +++ b/service/profile/endpoints/endpoint-domain.go @@ -6,8 +6,8 @@ import ( "regexp" "strings" - "github.com/safing/portmaster/intel" - "github.com/safing/portmaster/network/netutils" + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/service/network/netutils" ) const ( diff --git a/profile/endpoints/endpoint-ip.go b/service/profile/endpoints/endpoint-ip.go similarity index 94% rename from profile/endpoints/endpoint-ip.go rename to service/profile/endpoints/endpoint-ip.go index 9797eb8d..78706932 100644 --- a/profile/endpoints/endpoint-ip.go +++ b/service/profile/endpoints/endpoint-ip.go @@ -4,7 +4,7 @@ import ( "context" "net" - "github.com/safing/portmaster/intel" + "github.com/safing/portmaster/service/intel" ) // EndpointIP matches IPs. diff --git a/profile/endpoints/endpoint-iprange.go b/service/profile/endpoints/endpoint-iprange.go similarity index 94% rename from profile/endpoints/endpoint-iprange.go rename to service/profile/endpoints/endpoint-iprange.go index 6a0b713a..14503bd8 100644 --- a/profile/endpoints/endpoint-iprange.go +++ b/service/profile/endpoints/endpoint-iprange.go @@ -4,7 +4,7 @@ import ( "context" "net" - "github.com/safing/portmaster/intel" + "github.com/safing/portmaster/service/intel" ) // EndpointIPRange matches IP ranges. diff --git a/profile/endpoints/endpoint-lists.go b/service/profile/endpoints/endpoint-lists.go similarity index 95% rename from profile/endpoints/endpoint-lists.go rename to service/profile/endpoints/endpoint-lists.go index 58e48be7..8aedb0ee 100644 --- a/profile/endpoints/endpoint-lists.go +++ b/service/profile/endpoints/endpoint-lists.go @@ -4,7 +4,7 @@ import ( "context" "strings" - "github.com/safing/portmaster/intel" + "github.com/safing/portmaster/service/intel" ) // EndpointLists matches endpoint lists. diff --git a/profile/endpoints/endpoint-scopes.go b/service/profile/endpoints/endpoint-scopes.go similarity index 95% rename from profile/endpoints/endpoint-scopes.go rename to service/profile/endpoints/endpoint-scopes.go index c969b408..b506e2a1 100644 --- a/profile/endpoints/endpoint-scopes.go +++ b/service/profile/endpoints/endpoint-scopes.go @@ -4,8 +4,8 @@ import ( "context" "strings" - "github.com/safing/portmaster/intel" - "github.com/safing/portmaster/network/netutils" + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/service/network/netutils" ) const ( diff --git a/profile/endpoints/endpoint.go b/service/profile/endpoints/endpoint.go similarity index 98% rename from profile/endpoints/endpoint.go rename to service/profile/endpoints/endpoint.go index b893a634..962d78e2 100644 --- a/profile/endpoints/endpoint.go +++ b/service/profile/endpoints/endpoint.go @@ -6,8 +6,8 @@ import ( "strconv" "strings" - "github.com/safing/portmaster/intel" - "github.com/safing/portmaster/network/reference" + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/service/network/reference" ) // Endpoint describes an Endpoint Matcher. diff --git a/profile/endpoints/endpoint_test.go b/service/profile/endpoints/endpoint_test.go similarity index 100% rename from profile/endpoints/endpoint_test.go rename to service/profile/endpoints/endpoint_test.go diff --git a/profile/endpoints/endpoints.go b/service/profile/endpoints/endpoints.go similarity index 98% rename from profile/endpoints/endpoints.go rename to service/profile/endpoints/endpoints.go index 6ed3ad04..17649675 100644 --- a/profile/endpoints/endpoints.go +++ b/service/profile/endpoints/endpoints.go @@ -6,7 +6,7 @@ import ( "fmt" "strings" - "github.com/safing/portmaster/intel" + "github.com/safing/portmaster/service/intel" ) // Endpoints is a list of permitted or denied endpoints. diff --git a/profile/endpoints/endpoints_test.go b/service/profile/endpoints/endpoints_test.go similarity index 98% rename from profile/endpoints/endpoints_test.go rename to service/profile/endpoints/endpoints_test.go index 342d81d8..8dafe10d 100644 --- a/profile/endpoints/endpoints_test.go +++ b/service/profile/endpoints/endpoints_test.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/assert" - "github.com/safing/portmaster/core/pmtesting" - "github.com/safing/portmaster/intel" + "github.com/safing/portmaster/service/core/pmtesting" + "github.com/safing/portmaster/service/intel" ) func TestMain(m *testing.M) { diff --git a/profile/endpoints/reason.go b/service/profile/endpoints/reason.go similarity index 100% rename from profile/endpoints/reason.go rename to service/profile/endpoints/reason.go diff --git a/profile/fingerprint.go b/service/profile/fingerprint.go similarity index 100% rename from profile/fingerprint.go rename to service/profile/fingerprint.go diff --git a/profile/fingerprint_test.go b/service/profile/fingerprint_test.go similarity index 100% rename from profile/fingerprint_test.go rename to service/profile/fingerprint_test.go diff --git a/profile/framework.go b/service/profile/framework.go similarity index 100% rename from profile/framework.go rename to service/profile/framework.go diff --git a/profile/framework_test.go b/service/profile/framework_test.go similarity index 100% rename from profile/framework_test.go rename to service/profile/framework_test.go diff --git a/profile/get.go b/service/profile/get.go similarity index 100% rename from profile/get.go rename to service/profile/get.go diff --git a/profile/merge.go b/service/profile/merge.go similarity index 98% rename from profile/merge.go rename to service/profile/merge.go index 420d64f6..5e995182 100644 --- a/profile/merge.go +++ b/service/profile/merge.go @@ -7,7 +7,7 @@ import ( "time" "github.com/safing/portbase/database/record" - "github.com/safing/portmaster/profile/binmeta" + "github.com/safing/portmaster/service/profile/binmeta" ) // MergeProfiles merges multiple profiles into a new one. diff --git a/profile/meta.go b/service/profile/meta.go similarity index 100% rename from profile/meta.go rename to service/profile/meta.go diff --git a/profile/migrations.go b/service/profile/migrations.go similarity index 99% rename from profile/migrations.go rename to service/profile/migrations.go index e9b1344d..5eb94313 100644 --- a/profile/migrations.go +++ b/service/profile/migrations.go @@ -11,7 +11,7 @@ import ( "github.com/safing/portbase/database/migration" "github.com/safing/portbase/database/query" "github.com/safing/portbase/log" - "github.com/safing/portmaster/profile/binmeta" + "github.com/safing/portmaster/service/profile/binmeta" ) func registerMigrations() error { diff --git a/profile/module.go b/service/profile/module.go similarity index 93% rename from profile/module.go rename to service/profile/module.go index 547944b1..4465750d 100644 --- a/profile/module.go +++ b/service/profile/module.go @@ -10,9 +10,9 @@ import ( "github.com/safing/portbase/dataroot" "github.com/safing/portbase/log" "github.com/safing/portbase/modules" - _ "github.com/safing/portmaster/core/base" - "github.com/safing/portmaster/profile/binmeta" - "github.com/safing/portmaster/updates" + _ "github.com/safing/portmaster/service/core/base" + "github.com/safing/portmaster/service/profile/binmeta" + "github.com/safing/portmaster/service/updates" ) var ( diff --git a/profile/profile-layered-provider.go b/service/profile/profile-layered-provider.go similarity index 100% rename from profile/profile-layered-provider.go rename to service/profile/profile-layered-provider.go diff --git a/profile/profile-layered.go b/service/profile/profile-layered.go similarity index 99% rename from profile/profile-layered.go rename to service/profile/profile-layered.go index acd88da3..2635aed5 100644 --- a/profile/profile-layered.go +++ b/service/profile/profile-layered.go @@ -9,8 +9,8 @@ import ( "github.com/safing/portbase/database/record" "github.com/safing/portbase/log" "github.com/safing/portbase/runtime" - "github.com/safing/portmaster/intel" - "github.com/safing/portmaster/profile/endpoints" + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/service/profile/endpoints" ) // LayeredProfile combines multiple Profiles. diff --git a/profile/profile.go b/service/profile/profile.go similarity index 98% rename from profile/profile.go rename to service/profile/profile.go index 95e2b762..fff41908 100644 --- a/profile/profile.go +++ b/service/profile/profile.go @@ -15,9 +15,9 @@ import ( "github.com/safing/portbase/database/record" "github.com/safing/portbase/log" "github.com/safing/portbase/utils" - "github.com/safing/portmaster/intel/filterlists" - "github.com/safing/portmaster/profile/binmeta" - "github.com/safing/portmaster/profile/endpoints" + "github.com/safing/portmaster/service/intel/filterlists" + "github.com/safing/portmaster/service/profile/binmeta" + "github.com/safing/portmaster/service/profile/endpoints" ) // ProfileSource is the source of the profile. diff --git a/profile/special.go b/service/profile/special.go similarity index 100% rename from profile/special.go rename to service/profile/special.go diff --git a/resolver/api.go b/service/resolver/api.go similarity index 100% rename from resolver/api.go rename to service/resolver/api.go diff --git a/resolver/block-detection.go b/service/resolver/block-detection.go similarity index 100% rename from resolver/block-detection.go rename to service/resolver/block-detection.go diff --git a/resolver/compat.go b/service/resolver/compat.go similarity index 100% rename from resolver/compat.go rename to service/resolver/compat.go diff --git a/resolver/config.go b/service/resolver/config.go similarity index 98% rename from resolver/config.go rename to service/resolver/config.go index 135c7c27..b5538d7d 100644 --- a/resolver/config.go +++ b/service/resolver/config.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/safing/portbase/config" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/status" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/status" ) // Configuration Keys. @@ -23,7 +23,7 @@ var ( // We encourage everyone who has the technical abilities to set their own preferred servers. // For a list of configuration options, see - // https://github.com/safing/portmaster/wiki/DNS-Server-Settings + // https://github.com/safing/portmaster/service/wiki/DNS-Server-Settings // Quad9 (encrypted DNS) // "dot://dns.quad9.net?ip=9.9.9.9&name=Quad9&blockedif=empty", diff --git a/resolver/doc.go b/service/resolver/doc.go similarity index 100% rename from resolver/doc.go rename to service/resolver/doc.go diff --git a/resolver/failing.go b/service/resolver/failing.go similarity index 98% rename from resolver/failing.go rename to service/resolver/failing.go index 33cee5b5..2f1ff87b 100644 --- a/resolver/failing.go +++ b/service/resolver/failing.go @@ -6,7 +6,7 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/modules" - "github.com/safing/portmaster/netenv" + "github.com/safing/portmaster/service/netenv" ) var ( diff --git a/resolver/ipinfo.go b/service/resolver/ipinfo.go similarity index 100% rename from resolver/ipinfo.go rename to service/resolver/ipinfo.go diff --git a/resolver/ipinfo_test.go b/service/resolver/ipinfo_test.go similarity index 100% rename from resolver/ipinfo_test.go rename to service/resolver/ipinfo_test.go diff --git a/resolver/main.go b/service/resolver/main.go similarity index 97% rename from resolver/main.go rename to service/resolver/main.go index 2daab556..693797b5 100644 --- a/resolver/main.go +++ b/service/resolver/main.go @@ -14,9 +14,9 @@ import ( "github.com/safing/portbase/modules" "github.com/safing/portbase/notifications" "github.com/safing/portbase/utils/debug" - _ "github.com/safing/portmaster/core/base" - "github.com/safing/portmaster/intel" - "github.com/safing/portmaster/netenv" + _ "github.com/safing/portmaster/service/core/base" + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/service/netenv" ) var module *modules.Module diff --git a/resolver/main_test.go b/service/resolver/main_test.go similarity index 97% rename from resolver/main_test.go rename to service/resolver/main_test.go index 57168227..2a2dbe44 100644 --- a/resolver/main_test.go +++ b/service/resolver/main_test.go @@ -3,7 +3,7 @@ package resolver import ( "testing" - "github.com/safing/portmaster/core/pmtesting" + "github.com/safing/portmaster/service/core/pmtesting" ) var domainFeed = make(chan string) diff --git a/resolver/metrics.go b/service/resolver/metrics.go similarity index 100% rename from resolver/metrics.go rename to service/resolver/metrics.go diff --git a/resolver/namerecord.go b/service/resolver/namerecord.go similarity index 100% rename from resolver/namerecord.go rename to service/resolver/namerecord.go diff --git a/resolver/namerecord_test.go b/service/resolver/namerecord_test.go similarity index 100% rename from resolver/namerecord_test.go rename to service/resolver/namerecord_test.go diff --git a/resolver/resolve.go b/service/resolver/resolve.go similarity index 99% rename from resolver/resolve.go rename to service/resolver/resolve.go index b9feb0a9..fe3e11ff 100644 --- a/resolver/resolve.go +++ b/service/resolver/resolve.go @@ -14,7 +14,7 @@ import ( "github.com/safing/portbase/database" "github.com/safing/portbase/log" - "github.com/safing/portmaster/netenv" + "github.com/safing/portmaster/service/netenv" ) // Errors. diff --git a/resolver/resolver-env.go b/service/resolver/resolver-env.go similarity index 97% rename from resolver/resolver-env.go rename to service/resolver/resolver-env.go index d976d311..01f58ea7 100644 --- a/resolver/resolver-env.go +++ b/service/resolver/resolver-env.go @@ -9,8 +9,8 @@ import ( "github.com/miekg/dns" "github.com/safing/portbase/log" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/network/netutils" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network/netutils" ) const ( diff --git a/resolver/resolver-https.go b/service/resolver/resolver-https.go similarity index 98% rename from resolver/resolver-https.go rename to service/resolver/resolver-https.go index 2d40aac0..ed04bf92 100644 --- a/resolver/resolver-https.go +++ b/service/resolver/resolver-https.go @@ -14,7 +14,7 @@ import ( "github.com/miekg/dns" "github.com/safing/portbase/log" - "github.com/safing/portmaster/netenv" + "github.com/safing/portmaster/service/netenv" ) // HTTPSResolver is a resolver using just a single tcp connection with pipelining. diff --git a/resolver/resolver-mdns.go b/service/resolver/resolver-mdns.go similarity index 99% rename from resolver/resolver-mdns.go rename to service/resolver/resolver-mdns.go index 2e01122a..17f034c8 100644 --- a/resolver/resolver-mdns.go +++ b/service/resolver/resolver-mdns.go @@ -12,8 +12,8 @@ import ( "github.com/miekg/dns" "github.com/safing/portbase/log" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/network/netutils" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network/netutils" ) // DNS Classes. diff --git a/resolver/resolver-plain.go b/service/resolver/resolver-plain.go similarity index 98% rename from resolver/resolver-plain.go rename to service/resolver/resolver-plain.go index 614f30b2..56f85458 100644 --- a/resolver/resolver-plain.go +++ b/service/resolver/resolver-plain.go @@ -9,7 +9,7 @@ import ( "github.com/miekg/dns" "github.com/safing/portbase/log" - "github.com/safing/portmaster/netenv" + "github.com/safing/portmaster/service/netenv" ) var ( diff --git a/resolver/resolver-tcp.go b/service/resolver/resolver-tcp.go similarity index 99% rename from resolver/resolver-tcp.go rename to service/resolver/resolver-tcp.go index aed64e2d..271d8808 100644 --- a/resolver/resolver-tcp.go +++ b/service/resolver/resolver-tcp.go @@ -13,7 +13,7 @@ import ( "github.com/tevino/abool" "github.com/safing/portbase/log" - "github.com/safing/portmaster/netenv" + "github.com/safing/portmaster/service/netenv" ) const ( diff --git a/resolver/resolver.go b/service/resolver/resolver.go similarity index 98% rename from resolver/resolver.go rename to service/resolver/resolver.go index e899f480..3474fd30 100644 --- a/resolver/resolver.go +++ b/service/resolver/resolver.go @@ -11,8 +11,8 @@ import ( "github.com/tevino/abool" "github.com/safing/portbase/utils" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/network/netutils" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network/netutils" ) // DNS Resolver Attributes. diff --git a/resolver/resolver_test.go b/service/resolver/resolver_test.go similarity index 100% rename from resolver/resolver_test.go rename to service/resolver/resolver_test.go diff --git a/resolver/resolvers.go b/service/resolver/resolvers.go similarity index 99% rename from resolver/resolvers.go rename to service/resolver/resolvers.go index 10226b35..93edf2a1 100644 --- a/resolver/resolvers.go +++ b/service/resolver/resolvers.go @@ -15,8 +15,8 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/utils" - "github.com/safing/portmaster/netenv" - "github.com/safing/portmaster/network/netutils" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network/netutils" ) const maxSearchDomains = 100 diff --git a/resolver/resolvers_test.go b/service/resolver/resolvers_test.go similarity index 100% rename from resolver/resolvers_test.go rename to service/resolver/resolvers_test.go diff --git a/resolver/reverse.go b/service/resolver/reverse.go similarity index 100% rename from resolver/reverse.go rename to service/resolver/reverse.go diff --git a/resolver/reverse_test.go b/service/resolver/reverse_test.go similarity index 100% rename from resolver/reverse_test.go rename to service/resolver/reverse_test.go diff --git a/resolver/rr_context.go b/service/resolver/rr_context.go similarity index 100% rename from resolver/rr_context.go rename to service/resolver/rr_context.go diff --git a/resolver/rrcache.go b/service/resolver/rrcache.go similarity index 98% rename from resolver/rrcache.go rename to service/resolver/rrcache.go index 1b6fdc3d..36b46e31 100644 --- a/resolver/rrcache.go +++ b/service/resolver/rrcache.go @@ -9,8 +9,8 @@ import ( "github.com/miekg/dns" "github.com/safing/portbase/log" - "github.com/safing/portmaster/nameserver/nsutil" - "github.com/safing/portmaster/netenv" + "github.com/safing/portmaster/service/nameserver/nsutil" + "github.com/safing/portmaster/service/netenv" ) // RRCache is a single-use structure to hold a DNS response. diff --git a/resolver/rrcache_test.go b/service/resolver/rrcache_test.go similarity index 100% rename from resolver/rrcache_test.go rename to service/resolver/rrcache_test.go diff --git a/resolver/scopes.go b/service/resolver/scopes.go similarity index 99% rename from resolver/scopes.go rename to service/resolver/scopes.go index 044b83fc..ac1391b1 100644 --- a/resolver/scopes.go +++ b/service/resolver/scopes.go @@ -8,7 +8,7 @@ import ( "github.com/miekg/dns" "github.com/safing/portbase/log" - "github.com/safing/portmaster/netenv" + "github.com/safing/portmaster/service/netenv" ) // Domain Scopes. diff --git a/resolver/test/resolving.bash b/service/resolver/test/resolving.bash similarity index 100% rename from resolver/test/resolving.bash rename to service/resolver/test/resolving.bash diff --git a/status/module.go b/service/status/module.go similarity index 95% rename from status/module.go rename to service/status/module.go index bc823832..2465d09b 100644 --- a/status/module.go +++ b/service/status/module.go @@ -6,7 +6,7 @@ import ( "github.com/safing/portbase/modules" "github.com/safing/portbase/utils/debug" - "github.com/safing/portmaster/netenv" + "github.com/safing/portmaster/service/netenv" ) var module *modules.Module diff --git a/status/provider.go b/service/status/provider.go similarity index 95% rename from status/provider.go rename to service/status/provider.go index fbe8d84f..5130560e 100644 --- a/status/provider.go +++ b/service/status/provider.go @@ -3,7 +3,7 @@ package status import ( "github.com/safing/portbase/database/record" "github.com/safing/portbase/runtime" - "github.com/safing/portmaster/netenv" + "github.com/safing/portmaster/service/netenv" ) var pushUpdate runtime.PushFunc diff --git a/status/records.go b/service/status/records.go similarity index 92% rename from status/records.go rename to service/status/records.go index 63f3f9fd..56f19e5f 100644 --- a/status/records.go +++ b/service/status/records.go @@ -4,7 +4,7 @@ import ( "sync" "github.com/safing/portbase/database/record" - "github.com/safing/portmaster/netenv" + "github.com/safing/portmaster/service/netenv" ) // SystemStatusRecord describes the overall status of the Portmaster. diff --git a/status/security_level.go b/service/status/security_level.go similarity index 100% rename from status/security_level.go rename to service/status/security_level.go diff --git a/sync/module.go b/service/sync/module.go similarity index 100% rename from sync/module.go rename to service/sync/module.go diff --git a/sync/profile.go b/service/sync/profile.go similarity index 99% rename from sync/profile.go rename to service/sync/profile.go index bfc76893..22a6472b 100644 --- a/sync/profile.go +++ b/service/sync/profile.go @@ -13,8 +13,8 @@ import ( "github.com/safing/portbase/api" "github.com/safing/portbase/config" "github.com/safing/portbase/log" - "github.com/safing/portmaster/profile" - "github.com/safing/portmaster/profile/binmeta" + "github.com/safing/portmaster/service/profile" + "github.com/safing/portmaster/service/profile/binmeta" ) // ProfileExport holds an export of a profile. diff --git a/sync/setting_single.go b/service/sync/setting_single.go similarity index 99% rename from sync/setting_single.go rename to service/sync/setting_single.go index 24cd0cbc..8911d6e4 100644 --- a/sync/setting_single.go +++ b/service/sync/setting_single.go @@ -10,7 +10,7 @@ import ( "github.com/safing/portbase/api" "github.com/safing/portbase/config" "github.com/safing/portbase/formats/dsd" - "github.com/safing/portmaster/profile" + "github.com/safing/portmaster/service/profile" ) // SingleSettingExport holds an export of a single setting. diff --git a/sync/settings.go b/service/sync/settings.go similarity index 99% rename from sync/settings.go rename to service/sync/settings.go index 795d94bb..4e640d09 100644 --- a/sync/settings.go +++ b/service/sync/settings.go @@ -10,7 +10,7 @@ import ( "github.com/safing/portbase/api" "github.com/safing/portbase/config" - "github.com/safing/portmaster/profile" + "github.com/safing/portmaster/service/profile" ) // SettingsExport holds an export of settings. diff --git a/sync/util.go b/service/sync/util.go similarity index 100% rename from sync/util.go rename to service/sync/util.go diff --git a/ui/api.go b/service/ui/api.go similarity index 100% rename from ui/api.go rename to service/ui/api.go diff --git a/ui/module.go b/service/ui/module.go similarity index 100% rename from ui/module.go rename to service/ui/module.go diff --git a/ui/serve.go b/service/ui/serve.go similarity index 99% rename from ui/serve.go rename to service/ui/serve.go index 2fe7f710..1e9e5861 100644 --- a/ui/serve.go +++ b/service/ui/serve.go @@ -18,7 +18,7 @@ import ( "github.com/safing/portbase/modules" "github.com/safing/portbase/updater" "github.com/safing/portbase/utils" - "github.com/safing/portmaster/updates" + "github.com/safing/portmaster/service/updates" ) var ( diff --git a/updates/api.go b/service/updates/api.go similarity index 100% rename from updates/api.go rename to service/updates/api.go diff --git a/updates/assets/portmaster.service b/service/updates/assets/portmaster.service similarity index 100% rename from updates/assets/portmaster.service rename to service/updates/assets/portmaster.service diff --git a/updates/config.go b/service/updates/config.go similarity index 99% rename from updates/config.go rename to service/updates/config.go index a8fff098..c06e7793 100644 --- a/updates/config.go +++ b/service/updates/config.go @@ -7,7 +7,7 @@ import ( "github.com/safing/portbase/config" "github.com/safing/portbase/log" - "github.com/safing/portmaster/updates/helper" + "github.com/safing/portmaster/service/updates/helper" ) const cfgDevModeKey = "core/devMode" diff --git a/updates/export.go b/service/updates/export.go similarity index 99% rename from updates/export.go rename to service/updates/export.go index e17113d1..0f355d58 100644 --- a/updates/export.go +++ b/service/updates/export.go @@ -12,7 +12,7 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/updater" "github.com/safing/portbase/utils/debug" - "github.com/safing/portmaster/updates/helper" + "github.com/safing/portmaster/service/updates/helper" ) const ( diff --git a/updates/get.go b/service/updates/get.go similarity index 97% rename from updates/get.go rename to service/updates/get.go index 2cf7acf7..c133ae1f 100644 --- a/updates/get.go +++ b/service/updates/get.go @@ -4,7 +4,7 @@ import ( "path" "github.com/safing/portbase/updater" - "github.com/safing/portmaster/updates/helper" + "github.com/safing/portmaster/service/updates/helper" ) // GetPlatformFile returns the latest platform specific file identified by the given identifier. diff --git a/updates/helper/electron.go b/service/updates/helper/electron.go similarity index 100% rename from updates/helper/electron.go rename to service/updates/helper/electron.go diff --git a/updates/helper/indexes.go b/service/updates/helper/indexes.go similarity index 100% rename from updates/helper/indexes.go rename to service/updates/helper/indexes.go diff --git a/updates/helper/signing.go b/service/updates/helper/signing.go similarity index 100% rename from updates/helper/signing.go rename to service/updates/helper/signing.go diff --git a/updates/helper/updates.go b/service/updates/helper/updates.go similarity index 100% rename from updates/helper/updates.go rename to service/updates/helper/updates.go diff --git a/updates/main.go b/service/updates/main.go similarity index 99% rename from updates/main.go rename to service/updates/main.go index 02f46075..95c20f04 100644 --- a/updates/main.go +++ b/service/updates/main.go @@ -14,7 +14,7 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/modules" "github.com/safing/portbase/updater" - "github.com/safing/portmaster/updates/helper" + "github.com/safing/portmaster/service/updates/helper" ) const ( diff --git a/updates/notify.go b/service/updates/notify.go similarity index 100% rename from updates/notify.go rename to service/updates/notify.go diff --git a/updates/os_integration_default.go b/service/updates/os_integration_default.go similarity index 100% rename from updates/os_integration_default.go rename to service/updates/os_integration_default.go diff --git a/updates/os_integration_linux.go b/service/updates/os_integration_linux.go similarity index 100% rename from updates/os_integration_linux.go rename to service/updates/os_integration_linux.go diff --git a/updates/restart.go b/service/updates/restart.go similarity index 100% rename from updates/restart.go rename to service/updates/restart.go diff --git a/updates/state.go b/service/updates/state.go similarity index 100% rename from updates/state.go rename to service/updates/state.go diff --git a/updates/upgrader.go b/service/updates/upgrader.go similarity index 99% rename from updates/upgrader.go rename to service/updates/upgrader.go index d350b760..9467dc73 100644 --- a/updates/upgrader.go +++ b/service/updates/upgrader.go @@ -21,7 +21,7 @@ import ( "github.com/safing/portbase/rng" "github.com/safing/portbase/updater" "github.com/safing/portbase/utils/renameio" - "github.com/safing/portmaster/updates/helper" + "github.com/safing/portmaster/service/updates/helper" ) const ( diff --git a/spn/TESTING.md b/spn/TESTING.md new file mode 100644 index 00000000..88a82c33 --- /dev/null +++ b/spn/TESTING.md @@ -0,0 +1,26 @@ +# Testing SPN + +This page documents ways to test if the SPN works as intended. + +⚠ Work in Progress. Currently we are just collecting helpful things we find. + +## Test Multi-Identity Routing + +In order to test if the multi-identity routing is working, you can request multiple websites to display your public IP. +If they show different values, multi-identity routing is working. + +### Websites + +- +- +- +- + +### Terminal + +```sh +curl https://icanhazip.com +curl https://ipecho.net/plain +curl https://ipinfo.io/ip +curl https://ipinfo.tw/ip +``` diff --git a/spn/TRADEMARKS b/spn/TRADEMARKS new file mode 100644 index 00000000..1bff5e79 --- /dev/null +++ b/spn/TRADEMARKS @@ -0,0 +1,5 @@ +The names "Safing", "Portmaster", "SPN" and their logos are trademarks owned by Safing ICS Technologies GmbH (Austria). + +Although our code is free, it is very important that we strictly enforce our trademark rights, in order to be able to protect our users against people who use the marks to commit fraud. This means that, while you have considerable freedom to redistribute and modify our software, there are tight restrictions on your ability to use our names and logos in ways which fall in the domain of trademark law, even when built into binaries that we provide. + +This file is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. Parts of it were taken from https://www.mozilla.org/en-US/foundation/licensing/. diff --git a/spn/access/account/auth.go b/spn/access/account/auth.go new file mode 100644 index 00000000..d93e6bf5 --- /dev/null +++ b/spn/access/account/auth.go @@ -0,0 +1,65 @@ +package account + +import ( + "errors" + "net/http" +) + +// Authentication Headers. +const ( + AuthHeaderDevice = "Device-17" + AuthHeaderToken = "Token-17" + AuthHeaderNextToken = "Next-Token-17" + AuthHeaderNextTokenDeprecated = "Next_token_17" +) + +// Errors. +var ( + ErrMissingDeviceID = errors.New("missing device ID") + ErrMissingToken = errors.New("missing token") +) + +// AuthToken holds an authentication token. +type AuthToken struct { + Device string + Token string +} + +// GetAuthTokenFromRequest extracts an authentication token from a request. +func GetAuthTokenFromRequest(request *http.Request) (*AuthToken, error) { + device := request.Header.Get(AuthHeaderDevice) + if device == "" { + return nil, ErrMissingDeviceID + } + token := request.Header.Get(AuthHeaderToken) + if token == "" { + return nil, ErrMissingToken + } + + return &AuthToken{ + Device: device, + Token: token, + }, nil +} + +// ApplyTo applies the authentication token to a request. +func (at *AuthToken) ApplyTo(request *http.Request) { + request.Header.Set(AuthHeaderDevice, at.Device) + request.Header.Set(AuthHeaderToken, at.Token) +} + +// GetNextTokenFromResponse extracts an authentication token from a response. +func GetNextTokenFromResponse(resp *http.Response) (token string, ok bool) { + token = resp.Header.Get(AuthHeaderNextToken) + if token == "" { + // TODO: Remove when fixed on server. + token = resp.Header.Get(AuthHeaderNextTokenDeprecated) + } + + return token, token != "" +} + +// ApplyNextTokenToResponse applies the next authentication token to a response. +func ApplyNextTokenToResponse(w http.ResponseWriter, token string) { + w.Header().Set(AuthHeaderNextToken, token) +} diff --git a/spn/access/account/client.go b/spn/access/account/client.go new file mode 100644 index 00000000..d6d0f879 --- /dev/null +++ b/spn/access/account/client.go @@ -0,0 +1,14 @@ +package account + +// Customer Agent URLs. +const ( + CAAuthenticateURL = "/authenticate" + CAProfileURL = "/user/profile" + CAGetTokensURL = "/tokens" +) + +// Customer Hub URLs. +const ( + CHAuthenticateURL = "/v1/authenticate" + CHUserProfileURL = "/v1/user_profile" +) diff --git a/spn/access/account/types.go b/spn/access/account/types.go new file mode 100644 index 00000000..f92f9f65 --- /dev/null +++ b/spn/access/account/types.go @@ -0,0 +1,137 @@ +package account + +import ( + "time" + + "golang.org/x/exp/slices" +) + +// User, Subscription and Charge states. +const ( + // UserStateNone is only used within Portmaster for saving information for + // logging into the same device. + UserStateNone = "" + UserStateFresh = "fresh" + UserStateQueued = "queued" + UserStateApproved = "approved" + UserStateSuspended = "suspended" + UserStateLoggedOut = "loggedout" // Portmaster only. + + SubscriptionStateManual = "manual" // Manual renewal. + SubscriptionStateActive = "active" // Automatic renewal. + SubscriptionStateCancelled = "cancelled" // Automatic, but canceled. + + ChargeStatePending = "pending" + ChargeStateCompleted = "completed" + ChargeStateDead = "dead" +) + +// Agent and Hub return statuses. +const ( + // StatusInvalidAuth [401 Unauthorized] is returned when the credentials are + // invalid or the user was logged out. + StatusInvalidAuth = 401 + // StatusNoAccess [403 Forbidden] is returned when the user does not have + // an active subscription or the subscription does not include the required + // feature for the request. + StatusNoAccess = 403 + // StatusInvalidDevice [410 Gone] is returned when the device trying to + // log into does not exist. + StatusInvalidDevice = 410 + // StatusReachedDeviceLimit [409 Conflict] is returned when the device limit is reached. + StatusReachedDeviceLimit = 409 + // StatusDeviceInactive [423 Locked] is returned when the device is locked. + StatusDeviceInactive = 423 + // StatusNotLoggedIn [412 Precondition] is returned by the Portmaster, if an action required to be logged in, but the user is not logged in. + StatusNotLoggedIn = 412 + + // StatusUnknownError is a special status code that signifies an unknown or + // unexpected error by the API. + StatusUnknownError = -1 + // StatusConnectionError is a special status code that signifies a + // connection error. + StatusConnectionError = -2 +) + +// User describes an SPN user account. +type User struct { + Username string `json:"username"` + State string `json:"state"` + Balance int `json:"balance"` + Device *Device `json:"device"` + Subscription *Subscription `json:"subscription"` + CurrentPlan *Plan `json:"current_plan"` + NextPlan *Plan `json:"next_plan"` + View *View `json:"view"` +} + +// MayUseSPN returns whether the user may currently use the SPN. +func (u *User) MayUseSPN() bool { + return u.MayUse(FeatureSPN) +} + +// MayUsePrioritySupport returns whether the user may currently use the priority support. +func (u *User) MayUsePrioritySupport() bool { + return u.MayUse(FeatureSafingSupport) +} + +// MayUse returns whether the user may currently use the feature identified by +// the given feature ID. +// Leave feature ID empty to check without feature. +func (u *User) MayUse(featureID FeatureID) bool { + switch { + case u == nil: + // We need a user, obviously. + case u.State != UserStateApproved: + // Only approved users may use the SPN. + case u.Subscription == nil: + // Need a subscription. + case u.Subscription.EndsAt == nil: + case time.Now().After(*u.Subscription.EndsAt): + // Subscription needs to be active. + case u.CurrentPlan == nil: + // Need a plan / package. + case featureID != "" && + !slices.Contains(u.CurrentPlan.FeatureIDs, featureID): + // Required feature ID must be in plan / package feature IDs. + default: + // All checks passed! + return true + } + return false +} + +// Device describes a device of an SPN user. +type Device struct { + Name string `json:"name"` + ID string `json:"id"` +} + +// Subscription describes an SPN subscription. +type Subscription struct { + EndsAt *time.Time `json:"ends_at"` + State string `json:"state"` + NextBillingDate *time.Time `json:"next_billing_date"` + PaymentProvider string `json:"payment_provider"` +} + +// FeatureID defines a feature that requires a plan/subscription. +type FeatureID string + +// A list of all supported features. +const ( + FeatureSPN = FeatureID("spn") + FeatureSafingSupport = FeatureID("support") + FeatureHistory = FeatureID("history") + FeatureBWVis = FeatureID("bw-vis") + FeatureVPNCompat = FeatureID("vpn-compat") +) + +// Plan describes an SPN subscription plan. +type Plan struct { + Name string `json:"name"` + Amount int `json:"amount"` + Months int `json:"months"` + Renewable bool `json:"renewable"` + FeatureIDs []FeatureID `json:"feature_ids"` +} diff --git a/spn/access/account/view.go b/spn/access/account/view.go new file mode 100644 index 00000000..818bdfa6 --- /dev/null +++ b/spn/access/account/view.go @@ -0,0 +1,123 @@ +package account + +import ( + "fmt" + "strings" + "time" +) + +// View holds metadata that assists in displaying account information. +type View struct { + Message string + ShowAccountData bool + ShowAccountButton bool + ShowLoginButton bool + ShowRefreshButton bool + ShowLogoutButton bool +} + +// UpdateView updates the view and handles plan/package fallbacks. +func (u *User) UpdateView(requestStatusCode int) { + v := &View{} + + // Clean up naming and fallbacks when finished. + defer func() { + // Display "Free" package if no plan is set or if it expired. + switch { + case u.CurrentPlan == nil, + u.Subscription == nil, + u.Subscription.EndsAt == nil: + // Reset to free plan. + u.CurrentPlan = &Plan{ + Name: "Free", + } + u.Subscription = nil + + case u.Subscription.NextBillingDate != nil: + // Subscription is on auto-renew. + // Wait for update from server. + + case time.Since(*u.Subscription.EndsAt) > 0: + // Reset to free plan. + u.CurrentPlan = &Plan{ + Name: "Free", + } + u.Subscription = nil + } + + // Prepend "Portmaster " to plan name. + // TODO: Remove when Plan/Package naming has been updated. + if u.CurrentPlan != nil && !strings.HasPrefix(u.CurrentPlan.Name, "Portmaster ") { + u.CurrentPlan.Name = "Portmaster " + u.CurrentPlan.Name + } + + // Apply new view to user. + u.View = v + }() + + // Set view data based on return code. + switch requestStatusCode { + case StatusInvalidAuth, StatusInvalidDevice, StatusDeviceInactive: + // Account deleted or Device inactive or deleted. + // When using token based auth, there is no difference between these cases. + v.Message = "This device may have been deactivated or removed from your account. Please log in again." + v.ShowAccountData = true + v.ShowAccountButton = true + v.ShowLoginButton = true + v.ShowLogoutButton = true + return + + case StatusUnknownError: + v.Message = "There is an unknown error in the communication with the account server. The shown information may not be accurate. " + + case StatusConnectionError: + v.Message = "Portmaster could not connect to the account server. The shown information may not be accurate. " + } + + // Set view data based on profile data. + switch { + case u.State == UserStateLoggedOut: + // User logged out. + v.ShowAccountButton = true + v.ShowLoginButton = true + return + + case u.State == UserStateSuspended: + // Account is suspended. + v.Message += fmt.Sprintf("Your account (%s) was suspended. Please contact support for details.", u.Username) + v.ShowAccountButton = true + v.ShowRefreshButton = true + v.ShowLogoutButton = true + return + + case u.Subscription == nil || u.Subscription.EndsAt == nil: + // Account has never had a subscription. + v.Message += "Get more features. Upgrade today." + + case u.Subscription.NextBillingDate != nil: + switch { + case time.Since(*u.Subscription.NextBillingDate) > 0: + v.Message += "Your auto-renewal seems to be delayed. Please refresh and check the status of your payment. Payment information may be delayed." + case time.Until(*u.Subscription.NextBillingDate) < 24*time.Hour: + v.Message += "Your subscription will auto-renew soon. Please note that payment information may be delayed." + } + + case time.Since(*u.Subscription.EndsAt) > 0: + // Subscription expired. + if u.CurrentPlan != nil { + v.Message += fmt.Sprintf("Your package %s has ended. Extend it on the Account Page.", u.CurrentPlan.Name) + } else { + v.Message += "Your package has ended. Extend it on the Account Page." + } + + case time.Until(*u.Subscription.EndsAt) < 7*24*time.Hour: + // Add generic ending soon message if the package ends in less than 7 days. + v.Message += "Your package ends soon. Extend it on the Account Page." + } + + // Defaults for generally good accounts. + v.ShowAccountData = true + v.ShowAccountButton = true + v.ShowRefreshButton = true + v.ShowLogoutButton = true +} diff --git a/spn/access/api.go b/spn/access/api.go new file mode 100644 index 00000000..e38a8c9f --- /dev/null +++ b/spn/access/api.go @@ -0,0 +1,168 @@ +package access + +import ( + "fmt" + "net/http" + + "github.com/safing/portbase/api" + "github.com/safing/portbase/database/record" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/access/account" +) + +func registerAPIEndpoints() error { + if err := api.RegisterEndpoint(api.Endpoint{ + Path: `spn/account/login`, + Write: api.PermitAdmin, + WriteMethod: http.MethodPost, + HandlerFunc: handleLogin, + Name: "SPN Login", + Description: "Log into your SPN account.", + }); err != nil { + return err + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Path: `spn/account/logout`, + Write: api.PermitAdmin, + WriteMethod: http.MethodDelete, + ActionFunc: handleLogout, + Name: "SPN Logout", + Description: "Logout from your SPN account.", + Parameters: []api.Parameter{ + { + Method: http.MethodDelete, + Field: "purge", + Value: "", + Description: "If set, account data is purged. Otherwise, the username and device ID are kept in order to log into the same device when logging in with the same user again.", + }, + }, + }); err != nil { + return err + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Path: `spn/account/user/profile`, + Read: api.PermitUser, + ReadMethod: http.MethodGet, + RecordFunc: handleGetUserProfile, + Name: "SPN User Profile", + Description: "Get the user profile of the logged in SPN account.", + Parameters: []api.Parameter{ + { + Method: http.MethodGet, + Field: "refresh", + Value: "", + Description: "If set, the user profile is freshly fetched from the account server.", + }, + }, + }); err != nil { + return err + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Path: `account/features`, + Read: api.PermitUser, + ReadMethod: http.MethodGet, + StructFunc: func(_ *api.Request) (i interface{}, err error) { + return struct { + Features []Feature + }{ + Features: features, + }, nil + }, + Name: "Get Account Features", + Description: "Returns all account features.", + }); err != nil { + return err + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Path: `account/features/{id:[A-Za-z0-9_-]+}/icon`, + Read: api.PermitUser, + ReadMethod: http.MethodGet, + Name: "Returns the image of the featuare", + MimeType: "image/svg+xml", + DataFunc: func(ar *api.Request) (data []byte, err error) { + featureID, ok := ar.URLVars["id"] + if !ok { + return nil, fmt.Errorf("invalid feature id") + } + + for _, feature := range features { + if feature.ID == featureID { + return []byte(feature.icon), nil + } + } + + return nil, fmt.Errorf("feature id not found") + }, + }); err != nil { + return err + } + + return nil +} + +func handleLogin(w http.ResponseWriter, r *http.Request) { + // Get username and password. + username, password, ok := r.BasicAuth() + // Request, if omitted. + if !ok || username == "" || password == "" { + w.Header().Set("WWW-Authenticate", "Basic realm=SPN Login") + http.Error(w, "Login with your SPN account.", http.StatusUnauthorized) + return + } + + // Process login. + user, code, err := Login(username, password) + if err != nil { + log.Warningf("spn/access: failed to login: %s", err) + if code == 0 { + http.Error(w, "Internal error: "+err.Error(), http.StatusInternalServerError) + } else { + http.Error(w, err.Error(), code) + } + return + } + + // Return success. + _, _ = w.Write([]byte( + fmt.Sprintf("Now logged in as %s as device %s", user.Username, user.Device.Name), + )) +} + +func handleLogout(ar *api.Request) (msg string, err error) { + purge := ar.URL.Query().Get("purge") != "" + err = Logout(false, purge) + switch { + case err != nil: + log.Warningf("spn/access: failed to logout: %s", err) + return "", err + case purge: + return "Logged out and user data purged.", nil + default: + return "Logged out.", nil + } +} + +func handleGetUserProfile(ar *api.Request) (r record.Record, err error) { + // Check if we are already authenticated. + user, err := GetUser() + if err != nil || user.State == account.UserStateNone { + return nil, api.ErrorWithStatus( + ErrNotLoggedIn, + account.StatusInvalidAuth, + ) + } + + // Should we refresh the user profile? + if ar.URL.Query().Get("refresh") != "" { + user, _, err = UpdateUser() + if err != nil { + return nil, err + } + } + + return user, nil +} diff --git a/spn/access/client.go b/spn/access/client.go new file mode 100644 index 00000000..f22bb9e9 --- /dev/null +++ b/spn/access/client.go @@ -0,0 +1,550 @@ +package access + +import ( + "context" + "errors" + "fmt" + "net/http" + "sync" + "time" + + "github.com/safing/portbase/database" + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/access/account" + "github.com/safing/portmaster/spn/access/token" +) + +// Client URLs. +const ( + AccountServer = "https://api.account.safing.io" + LoginPath = "/api/v1/authenticate" + UserProfilePath = "/api/v1/user/profile" + TokenRequestSetupPath = "/api/v1/token/request/setup" //nolint:gosec + TokenRequestIssuePath = "/api/v1/token/request/issue" //nolint:gosec + HealthCheckPath = "/api/v1/health" + + defaultDataFormat = dsd.CBOR + defaultRequestTimeout = 30 * time.Second +) + +var ( + accountClient = &http.Client{} + clientRequestLock sync.Mutex + + // EnableAfterLogin automatically enables the SPN subsystem/module after login. + EnableAfterLogin = true +) + +type clientRequestOptions struct { + method string + url string + send interface{} + recv interface{} + requestTimeout time.Duration + dataFormat uint8 + setAuthToken bool + requireNextAuthToken bool + logoutOnAuthError bool + requestSetupFunc func(*http.Request) error +} + +func makeClientRequest(opts *clientRequestOptions) (resp *http.Response, err error) { + // Get request timeout. + if opts.requestTimeout == 0 { + opts.requestTimeout = defaultRequestTimeout + } + // Get context for request. + var ctx context.Context + var cancel context.CancelFunc + if module.Online() { + // Only use module context if online. + ctx, cancel = context.WithTimeout(module.Ctx, opts.requestTimeout) + defer cancel() + } else { + // Otherwise, use the background context. + ctx, cancel = context.WithTimeout(context.Background(), opts.requestTimeout) + defer cancel() + } + + // Create new request. + request, err := http.NewRequestWithContext(ctx, opts.method, opts.url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request structure: %w", err) + } + + // Prepare body and content type. + if opts.dataFormat == dsd.AUTO { + opts.dataFormat = defaultDataFormat + } + if opts.send != nil { + // Add data to body. + err = dsd.DumpToHTTPRequest(request, opts.send, opts.dataFormat) + if err != nil { + return nil, fmt.Errorf("failed to add request body: %w", err) + } + } else { + // Set requested HTTP response format. + _, err = dsd.RequestHTTPResponseFormat(request, opts.dataFormat) + if err != nil { + return nil, fmt.Errorf("failed to set requested response format: %w", err) + } + } + + // Get auth token to apply to request. + var authToken *AuthTokenRecord + if opts.setAuthToken { + authToken, err = GetAuthToken() + if err != nil { + return nil, ErrNotLoggedIn + } + authToken.Token.ApplyTo(request) + } + + // Do any additional custom request setup. + if opts.requestSetupFunc != nil { + err = opts.requestSetupFunc(request) + if err != nil { + return nil, err + } + } + + // Make request. + resp, err = accountClient.Do(request) + if err != nil { + updateUserWithFailedRequest(account.StatusConnectionError, false) + tokenIssuerFailed() + return nil, fmt.Errorf("http request failed: %w", err) + } + log.Debugf("spn/access: request to %s returned %s", request.URL, resp.Status) + defer func() { + _ = resp.Body.Close() + }() + // Handle request error. + switch resp.StatusCode { + case http.StatusOK, http.StatusCreated: + // All good! + + case account.StatusInvalidAuth, account.StatusInvalidDevice: + // Wrong username / password. + updateUserWithFailedRequest(resp.StatusCode, true) + return resp, ErrInvalidCredentials + + case account.StatusReachedDeviceLimit: + // Device limit is reached. + updateUserWithFailedRequest(resp.StatusCode, true) + return resp, ErrDeviceLimitReached + + case account.StatusDeviceInactive: + // Device is locked. + updateUserWithFailedRequest(resp.StatusCode, true) + return resp, ErrDeviceIsLocked + + default: + updateUserWithFailedRequest(account.StatusUnknownError, false) + tokenIssuerFailed() + return resp, fmt.Errorf("unexpected reply: [%d] %s", resp.StatusCode, resp.Status) + } + + // Save next auth token. + if authToken != nil { + err = authToken.Update(resp) + if err != nil { + if errors.Is(err, account.ErrMissingToken) { + if opts.requireNextAuthToken { + return resp, fmt.Errorf("failed to save next auth token: %w", err) + } + } else { + return resp, fmt.Errorf("failed to save next auth token: %w", err) + } + } + } else if opts.requireNextAuthToken { + return resp, fmt.Errorf("failed to save next auth token: %w", account.ErrMissingToken) + } + + // Load response data. + if opts.recv != nil { + _, err = dsd.LoadFromHTTPResponse(resp, opts.recv) + if err != nil { + return resp, fmt.Errorf("failed to parse response: %w", err) + } + } + + tokenIssuerIsFailing.UnSet() + return resp, nil +} + +func updateUserWithFailedRequest(statusCode int, disableSubscription bool) { + // Get user from database. + user, err := GetUser() + if err != nil { + if !errors.Is(err, ErrNotLoggedIn) { + log.Warningf("spn/access: failed to get user to update with failed request: %s", err) + } + return + } + + func() { + user.Lock() + defer user.Unlock() + + // Ignore update if user state is undefined or logged out. + if user.State == "" || user.State == account.UserStateLoggedOut { + return + } + + // Disable the subscription if desired. + if disableSubscription && user.Subscription != nil { + user.Subscription.EndsAt = nil + } + + // Update view with the status code and save user. + user.UpdateView(statusCode) + }() + + err = user.Save() + if err != nil { + log.Warningf("spn/access: failed to save user after update with failed request: %s", err) + } +} + +// Login logs the user into the SPN account with the given username and password. +func Login(username, password string) (user *UserRecord, code int, err error) { + clientRequestLock.Lock() + defer clientRequestLock.Unlock() + + // Trigger account update when done. + defer module.TriggerEvent(AccountUpdateEvent, nil) + + // Get previous user. + previousUser, err := GetUser() + if err != nil { + if !errors.Is(err, ErrNotLoggedIn) { + log.Warningf("spn/access: failed to get previous for re-login: %s", err) + } + previousUser = nil + } + + // Create request options. + userAccount := &account.User{} + requestOptions := &clientRequestOptions{ + method: http.MethodPost, + url: AccountServer + LoginPath, + recv: userAccount, + dataFormat: dsd.JSON, + requestSetupFunc: func(request *http.Request) error { + // Add username and password. + request.SetBasicAuth(username, password) + + // Try to reuse the device ID, if the username matches the previous user. + if previousUser != nil && username == previousUser.Username { + request.Header.Set(account.AuthHeaderDevice, previousUser.Device.ID) + } + + return nil + }, + } + + // Make request. + resp, err := makeClientRequest(requestOptions) //nolint:bodyclose // Body is closed in function. + if err != nil { + if resp != nil && resp.StatusCode == account.StatusInvalidDevice { + // Try again without the previous device ID. + previousUser = nil + log.Info("spn/access: retrying log in without re-using previous device ID") + resp, err = makeClientRequest(requestOptions) //nolint:bodyclose // Body is closed in function. + } + if err != nil { + if resp != nil { + return nil, resp.StatusCode, err + } + return nil, 0, err + } + } + + // Save new user. + now := time.Now() + user = &UserRecord{ + User: userAccount, + LoggedInAt: &now, + } + + user.UpdateView(0) + err = user.Save() + if err != nil { + return user, resp.StatusCode, fmt.Errorf("failed to save new user profile: %w", err) + } + + // Save initial auth token. + err = SaveNewAuthToken(user.Device.ID, resp) + if err != nil { + return user, resp.StatusCode, fmt.Errorf("failed to save initial auth token: %w", err) + } + + // Enable the SPN right after login. + if user.MayUseSPN() && EnableAfterLogin { + enableSPN() + } + + log.Infof("spn/access: logged in as %q on device %q", user.Username, user.Device.Name) + return user, resp.StatusCode, nil +} + +// Logout logs the user out of the SPN account. +// Specify "shallow" to keep user data in order to display data in the +// UI - preferably when logged out be the server. +// Specify "purge" in order to fully delete all user account data, even +// the device ID so that logging in again will create a new device. +func Logout(shallow, purge bool) error { + clientRequestLock.Lock() + defer clientRequestLock.Unlock() + + // Trigger account update when done. + defer module.TriggerEvent(AccountUpdateEvent, nil) + + // Clear caches. + clearUserCaches() + + // Clear tokens. + clearTokens() + + // Delete auth token. + err := db.Delete(authTokenRecordKey) + if err != nil && !errors.Is(err, database.ErrNotFound) { + return fmt.Errorf("failed to delete auth token: %w", err) + } + + // Delete all user data if purging. + if purge { + err := db.Delete(userRecordKey) + if err != nil && !errors.Is(err, database.ErrNotFound) { + return fmt.Errorf("failed to delete user: %w", err) + } + + // Disable SPN when the user logs out directly. + disableSPN() + + log.Info("spn/access: logged out and purged data") + return nil + } + + // Else, just update the user. + user, err := GetUser() + if err != nil { + if errors.Is(err, ErrNotLoggedIn) { + return nil + } + return fmt.Errorf("failed to load user for logout: %w", err) + } + + func() { + user.Lock() + defer user.Unlock() + + if shallow { + // Shallow logout: User stays logged in the UI to display status when + // logged out from the Portmaster or Customer Hub. + user.User.State = account.UserStateLoggedOut + } else { + // Proper logout: User is logged out from UI. + // Reset all user data, except for username and device ID in order to log + // into the same device again. + user.User = &account.User{ + Username: user.Username, + Device: &account.Device{ + ID: user.Device.ID, + }, + } + user.LoggedInAt = &time.Time{} + } + user.UpdateView(0) + }() + err = user.Save() + if err != nil { + return fmt.Errorf("failed to save user for logout: %w", err) + } + + if shallow { + log.Info("spn/access: logged out shallow") + } else { + log.Info("spn/access: logged out") + + // Disable SPN when the user logs out directly. + disableSPN() + } + + return nil +} + +// UpdateUser fetches the current user information from the server. +func UpdateUser() (user *UserRecord, statusCode int, err error) { + clientRequestLock.Lock() + defer clientRequestLock.Unlock() + + // Trigger account update when done. + defer module.TriggerEvent(AccountUpdateEvent, nil) + + // Create request options. + userData := &account.User{} + requestOptions := &clientRequestOptions{ + method: http.MethodGet, + url: AccountServer + UserProfilePath, + recv: userData, + dataFormat: dsd.JSON, + setAuthToken: true, + requireNextAuthToken: true, + logoutOnAuthError: true, + } + + // Make request. + resp, err := makeClientRequest(requestOptions) //nolint:bodyclose // Body is closed in function. + if err != nil { + if resp != nil { + return nil, resp.StatusCode, err + } + return nil, 0, err + } + + // Save to previous user, if exists. + previousUser, err := GetUser() + if err == nil { + func() { + previousUser.Lock() + defer previousUser.Unlock() + previousUser.User = userData + previousUser.UpdateView(resp.StatusCode) + }() + err := previousUser.Save() + if err != nil { + log.Warningf("spn/access: failed to save updated user profile: %s", err) + } + + // Notify user of nearing end of package. + notifyOfPackageEnd(previousUser) + + log.Infof("spn/access: got user profile, updated existing") + return previousUser, resp.StatusCode, nil + } + + // Else, save as new user. + now := time.Now() + newUser := &UserRecord{ + User: userData, + LoggedInAt: &now, + } + newUser.UpdateView(resp.StatusCode) + err = newUser.Save() + if err != nil { + log.Warningf("spn/access: failed to save new user profile: %s", err) + } + + // Notify user of nearing end of package. + notifyOfPackageEnd(newUser) + + log.Infof("spn/access: got user profile, saved as new") + return newUser, resp.StatusCode, nil +} + +// UpdateTokens fetches more tokens for handlers that need it. +func UpdateTokens() error { + clientRequestLock.Lock() + defer clientRequestLock.Unlock() + + // Check if the user may request tokens. + user, err := GetUser() + if err != nil { + return fmt.Errorf("failed to get user: %w", err) + } + if !user.MayUseTheSPN() { + return ErrMayNotUseSPN + } + + // Create setup request, return if not required. + setupRequest, setupRequired := token.CreateSetupRequest() + var setupResponse *token.SetupResponse + if setupRequired { + // Request setup data. + setupResponse = &token.SetupResponse{} + _, err := makeClientRequest(&clientRequestOptions{ //nolint:bodyclose // Body is closed in function. + method: http.MethodPost, + url: AccountServer + TokenRequestSetupPath, + send: setupRequest, + recv: setupResponse, + dataFormat: dsd.MsgPack, + setAuthToken: true, + logoutOnAuthError: true, + }) + if err != nil { + return fmt.Errorf("failed to request setup data: %w", err) + } + } + + // Create request for issuing new tokens. + tokenRequest, requestRequired, err := token.CreateTokenRequest(setupResponse) + if err != nil { + return fmt.Errorf("failed to create token request: %w", err) + } + if !requestRequired { + return nil + } + + // Request issuing new tokens. + issuedTokens := &token.IssuedTokens{} + _, err = makeClientRequest(&clientRequestOptions{ //nolint:bodyclose // Body is closed in function. + method: http.MethodPost, + url: AccountServer + TokenRequestIssuePath, + send: tokenRequest, + recv: issuedTokens, + dataFormat: dsd.MsgPack, + setAuthToken: true, + logoutOnAuthError: true, + }) + if err != nil { + return fmt.Errorf("failed to request tokens: %w", err) + } + + // Save tokens to handlers. + err = token.ProcessIssuedTokens(issuedTokens) + if err != nil { + return fmt.Errorf("failed to process issued tokens: %w", err) + } + + // Log new status. + regular, fallback := GetTokenAmount(ExpandAndConnectZones) + log.Infof( + "spn/access: got new tokens, now at %d regular and %d fallback tokens for expand and connect", + regular, + fallback, + ) + + return nil +} + +var ( + lastHealthCheckExpires time.Time + lastHealthCheckLock sync.Mutex + lastHealthCheckValidityDuration = 30 * time.Second +) + +func healthCheck() (ok bool) { + lastHealthCheckLock.Lock() + defer lastHealthCheckLock.Unlock() + + // Return current value if recently checked. + if time.Now().Before(lastHealthCheckExpires) { + return tokenIssuerIsFailing.IsNotSet() + } + + // Check health. + _, err := makeClientRequest(&clientRequestOptions{ //nolint:bodyclose // Body is closed in function. + method: http.MethodGet, + url: AccountServer + HealthCheckPath, + }) + if err != nil { + log.Warningf("spn/access: token issuer health check failed: %s", err) + } + // Update health check expiry. + lastHealthCheckExpires = time.Now().Add(lastHealthCheckValidityDuration) + + return tokenIssuerIsFailing.IsNotSet() +} diff --git a/spn/access/client_test.go b/spn/access/client_test.go new file mode 100644 index 00000000..93c5e81e --- /dev/null +++ b/spn/access/client_test.go @@ -0,0 +1,79 @@ +package access + +import ( + "os" + "testing" +) + +var ( + testUsername = os.Getenv("SPN_TEST_USERNAME") + testPassword = os.Getenv("SPN_TEST_PASSWORD") +) + +func TestClient(t *testing.T) { + // Skip test in CI. + if testing.Short() { + t.Skip() + } + t.Parallel() + + if testUsername == "" || testPassword == "" { + t.Fatal("test username or password not configured") + } + + loginAndRefresh(t, true, 5) + clearUserCaches() + loginAndRefresh(t, false, 1) + + err := Logout(false, false) + if err != nil { + t.Fatalf("failed to log out: %s", err) + } + t.Logf("logged out") + + loginAndRefresh(t, true, 1) + + err = Logout(false, true) + if err != nil { + t.Fatalf("failed to log out: %s", err) + } + t.Logf("logged out with purge") + + loginAndRefresh(t, true, 1) +} + +func loginAndRefresh(t *testing.T, doLogin bool, refreshTimes int) { + t.Helper() + + if doLogin { + _, _, err := Login(testUsername, testPassword) + if err != nil { + t.Fatalf("login failed: %s", err) + } + user, err := GetUser() + if err != nil { + t.Fatalf("failed to get user: %s", err) + } + t.Logf("user (from login): %+v", user.User) + t.Logf("device (from login): %+v", user.User.Device) + authToken, err := GetAuthToken() + if err != nil { + t.Fatalf("failed to get auth token: %s", err) + } + t.Logf("auth token: %+v", authToken.Token) + } + + for i := 0; i < refreshTimes; i++ { + user, _, err := UpdateUser() + if err != nil { + t.Fatalf("getting profile failed: %s", err) + } + t.Logf("user (from refresh): %+v", user.User) + + authToken, err := GetAuthToken() + if err != nil { + t.Fatalf("failed to get auth token: %s", err) + } + t.Logf("auth token: %+v", authToken.Token) + } +} diff --git a/spn/access/database.go b/spn/access/database.go new file mode 100644 index 00000000..be5ea95a --- /dev/null +++ b/spn/access/database.go @@ -0,0 +1,258 @@ +package access + +import ( + "errors" + "fmt" + "net/http" + "sync" + "time" + + "github.com/safing/portbase/database" + "github.com/safing/portbase/database/record" + "github.com/safing/portmaster/spn/access/account" +) + +const ( + userRecordKey = "core:spn/account/user" + authTokenRecordKey = "core:spn/account/authtoken" //nolint:gosec // Not a credential. + tokenStorageKeyTemplate = "core:spn/account/tokens/%s" //nolint:gosec // Not a credential. +) + +var db = database.NewInterface(&database.Options{ + Local: true, + Internal: true, +}) + +// UserRecord holds a SPN user account. +type UserRecord struct { + record.Base + sync.Mutex + + *account.User + + LastNotifiedOfEnd *time.Time + LoggedInAt *time.Time +} + +// MayUseSPN returns whether the user may currently use the SPN. +func (user *UserRecord) MayUseSPN() bool { + // Shadow this function in order to allow calls on a nil user. + if user == nil || user.User == nil { + return false + } + return user.User.MayUseSPN() +} + +// MayUsePrioritySupport returns whether the user may currently use the priority support. +func (user *UserRecord) MayUsePrioritySupport() bool { + // Shadow this function in order to allow calls on a nil user. + if user == nil || user.User == nil { + return false + } + return user.User.MayUsePrioritySupport() +} + +// MayUse returns whether the user may currently use the feature identified by +// the given feature ID. +// Leave feature ID empty to check without feature. +func (user *UserRecord) MayUse(featureID account.FeatureID) bool { + // Shadow this function in order to allow calls on a nil user. + if user == nil || user.User == nil { + return false + } + return user.User.MayUse(featureID) +} + +// AuthTokenRecord holds an authentication token. +type AuthTokenRecord struct { + record.Base + sync.Mutex + + Token *account.AuthToken +} + +// GetToken returns the token from the record. +func (authToken *AuthTokenRecord) GetToken() *account.AuthToken { + authToken.Lock() + defer authToken.Unlock() + + return authToken.Token +} + +// SaveNewAuthToken saves a new auth token to the database. +func SaveNewAuthToken(deviceID string, resp *http.Response) error { + token, ok := account.GetNextTokenFromResponse(resp) + if !ok { + return account.ErrMissingToken + } + + newAuthToken := &AuthTokenRecord{ + Token: &account.AuthToken{ + Device: deviceID, + Token: token, + }, + } + return newAuthToken.Save() +} + +// Update updates an existing auth token with the next token from a response. +func (authToken *AuthTokenRecord) Update(resp *http.Response) error { + token, ok := account.GetNextTokenFromResponse(resp) + if !ok { + return account.ErrMissingToken + } + + // Update token with new account.AuthToken. + func() { + authToken.Lock() + defer authToken.Unlock() + + authToken.Token = &account.AuthToken{ + Device: authToken.Token.Device, + Token: token, + } + }() + + return authToken.Save() +} + +var ( + accountCacheLock sync.Mutex + + cachedUser *UserRecord + cachedUserSet bool + + cachedAuthToken *AuthTokenRecord +) + +func clearUserCaches() { + accountCacheLock.Lock() + defer accountCacheLock.Unlock() + + cachedUser = nil + cachedUserSet = false + cachedAuthToken = nil +} + +// GetUser returns the current user account. +// Returns nil when no user is logged in. +func GetUser() (*UserRecord, error) { + // Check cache. + accountCacheLock.Lock() + defer accountCacheLock.Unlock() + if cachedUserSet { + if cachedUser == nil { + return nil, ErrNotLoggedIn + } + return cachedUser, nil + } + + // Load from disk. + r, err := db.Get(userRecordKey) + if err != nil { + if errors.Is(err, database.ErrNotFound) { + cachedUser = nil + cachedUserSet = true + return nil, ErrNotLoggedIn + } + return nil, err + } + + // Unwrap record. + if r.IsWrapped() { + // only allocate a new struct, if we need it + newUser := &UserRecord{} + err = record.Unwrap(r, newUser) + if err != nil { + return nil, err + } + cachedUser = newUser + cachedUserSet = true + return cachedUser, nil + } + + // Or adjust type. + newUser, ok := r.(*UserRecord) + if !ok { + return nil, fmt.Errorf("record not of type *UserRecord, but %T", r) + } + cachedUser = newUser + cachedUserSet = true + return cachedUser, nil +} + +// Save saves the User. +func (user *UserRecord) Save() error { + // Update cache. + accountCacheLock.Lock() + defer accountCacheLock.Unlock() + cachedUser = user + cachedUserSet = true + + // Update view if unset. + if user.View == nil { + user.UpdateView(0) + } + + // Set, check and update metadata. + if !user.KeyIsSet() { + user.SetKey(userRecordKey) + } + user.UpdateMeta() + + return db.Put(user) +} + +// GetAuthToken returns the current auth token. +func GetAuthToken() (*AuthTokenRecord, error) { + // Check cache. + accountCacheLock.Lock() + defer accountCacheLock.Unlock() + if cachedAuthToken != nil { + return cachedAuthToken, nil + } + + // Load from disk. + r, err := db.Get(authTokenRecordKey) + if err != nil { + return nil, err + } + + // Unwrap record. + if r.IsWrapped() { + // only allocate a new struct, if we need it + newAuthRecord := &AuthTokenRecord{} + err = record.Unwrap(r, newAuthRecord) + if err != nil { + return nil, err + } + cachedAuthToken = newAuthRecord + return newAuthRecord, nil + } + + // Or adjust type. + newAuthRecord, ok := r.(*AuthTokenRecord) + if !ok { + return nil, fmt.Errorf("record not of type *AuthTokenRecord, but %T", r) + } + cachedAuthToken = newAuthRecord + return newAuthRecord, nil +} + +// Save saves the auth token to the database. +func (authToken *AuthTokenRecord) Save() error { + // Update cache. + accountCacheLock.Lock() + defer accountCacheLock.Unlock() + cachedAuthToken = authToken + + // Set, check and update metadata. + if !authToken.KeyIsSet() { + authToken.SetKey(authTokenRecordKey) + } + authToken.UpdateMeta() + authToken.Meta().MakeSecret() + authToken.Meta().MakeCrownJewel() + + return db.Put(authToken) +} diff --git a/spn/access/features.go b/spn/access/features.go new file mode 100644 index 00000000..a26805e1 --- /dev/null +++ b/spn/access/features.go @@ -0,0 +1,127 @@ +package access + +import "github.com/safing/portmaster/spn/access/account" + +// Feature describes a notable part of the program. +type Feature struct { + Name string + ID string + RequiredFeatureID account.FeatureID + ConfigKey string + ConfigScope string + InPackage *Package + Comment string + Beta bool + ComingSoon bool + icon string +} + +// Package combines a set of features. +type Package struct { + Name string + HexColor string + InfoURL string +} + +var ( + infoURL = "https://safing.io/pricing/" + packageFree = &Package{ + Name: "Free", + HexColor: "#ffffff", + InfoURL: infoURL, + } + packagePlus = &Package{ + Name: "Plus", + HexColor: "#2fcfae", + InfoURL: infoURL, + } + packagePro = &Package{ + Name: "Pro", + HexColor: "#029ad0", + InfoURL: infoURL, + } + features = []Feature{ + { + Name: "Secure DNS", + ID: "dns", + ConfigScope: "dns/", + InPackage: packageFree, + icon: ` + + + + `, + }, + { + Name: "Privacy Filter", + ID: "filter", + ConfigScope: "filter/", + InPackage: packageFree, + icon: ` + + + + `, + }, + { + Name: "Network History", + ID: string(account.FeatureHistory), + RequiredFeatureID: account.FeatureHistory, + ConfigKey: "history/enable", + ConfigScope: "history/", + InPackage: packagePlus, + icon: ` + + + + `, + }, + { + Name: "Bandwidth Visibility", + ID: string(account.FeatureBWVis), + RequiredFeatureID: account.FeatureBWVis, + InPackage: packagePlus, + Beta: true, + icon: ` + + + + `, + }, + { + Name: "Safing Support", + ID: string(account.FeatureSafingSupport), + RequiredFeatureID: account.FeatureSafingSupport, + InPackage: packagePlus, + icon: ` + + + + `, + }, + { + Name: "Safing Privacy Network", + ID: string(account.FeatureSPN), + RequiredFeatureID: account.FeatureSPN, + ConfigKey: "spn/enable", + ConfigScope: "spn/", + InPackage: packagePro, + icon: ` + + + + + + + + + `, + }, + } +) diff --git a/spn/access/module.go b/spn/access/module.go new file mode 100644 index 00000000..3f935f33 --- /dev/null +++ b/spn/access/module.go @@ -0,0 +1,194 @@ +package access + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/tevino/abool" + + "github.com/safing/portbase/config" + "github.com/safing/portbase/log" + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/spn/access/account" + "github.com/safing/portmaster/spn/access/token" + "github.com/safing/portmaster/spn/conf" +) + +var ( + module *modules.Module + + accountUpdateTask *modules.Task + + tokenIssuerIsFailing = abool.New() + tokenIssuerRetryDuration = 10 * time.Minute + + // AccountUpdateEvent is fired when the account has changed in any way. + AccountUpdateEvent = "account update" +) + +// Errors. +var ( + ErrDeviceIsLocked = errors.New("device is locked") + ErrDeviceLimitReached = errors.New("device limit reached") + ErrFallbackNotAvailable = errors.New("fallback tokens not available, token issuer is online") + ErrInvalidCredentials = errors.New("invalid credentials") + ErrMayNotUseSPN = errors.New("may not use SPN") + ErrNotLoggedIn = errors.New("not logged in") +) + +func init() { + module = modules.Register("access", prep, start, stop, "terminal") +} + +func prep() error { + module.RegisterEvent(AccountUpdateEvent, true) + + // Register API handlers. + if conf.Client() { + err := registerAPIEndpoints() + if err != nil { + return err + } + } + + return nil +} + +func start() error { + // Initialize zones. + if err := InitializeZones(); err != nil { + return err + } + + if conf.Client() { + // Load tokens from database. + loadTokens() + + // Register new task. + accountUpdateTask = module.NewTask( + "update account", + UpdateAccount, + ).Repeat(24 * time.Hour).Schedule(time.Now().Add(1 * time.Minute)) + } + + return nil +} + +func stop() error { + if conf.Client() { + // Stop account update task. + accountUpdateTask.Cancel() + accountUpdateTask = nil + + // Store tokens to database. + storeTokens() + } + + // Reset zones. + token.ResetRegistry() + + return nil +} + +// UpdateAccount updates the user account and fetches new tokens, if needed. +func UpdateAccount(_ context.Context, task *modules.Task) error { + // Retry sooner if the token issuer is failing. + defer func() { + if tokenIssuerIsFailing.IsSet() && task != nil { + task.Schedule(time.Now().Add(tokenIssuerRetryDuration)) + } + }() + + // Get current user. + u, err := GetUser() + if err == nil { + // Do not update if we just updated. + if time.Since(time.Unix(u.Meta().Modified, 0)) < 2*time.Minute { + return nil + } + } + + u, _, err = UpdateUser() + if err != nil { + return fmt.Errorf("failed to update user profile: %w", err) + } + + err = UpdateTokens() + if err != nil { + return fmt.Errorf("failed to get tokens: %w", err) + } + + // Schedule next check. + switch { + case u == nil: // No user. + case u.Subscription == nil: // No subscription. + case u.Subscription.EndsAt == nil: // Subscription not active + + case time.Until(*u.Subscription.EndsAt) < 24*time.Hour && + time.Since(*u.Subscription.EndsAt) < 24*time.Hour: + // Update account every hour 24h hours before and after the subscription ends. + task.Schedule(time.Now().Add(time.Hour)) + + case u.Subscription.NextBillingDate == nil: // No auto-subscription. + + case time.Until(*u.Subscription.NextBillingDate) < 24*time.Hour && + time.Since(*u.Subscription.NextBillingDate) < 24*time.Hour: + // Update account every hour 24h hours before and after the next billing date. + task.Schedule(time.Now().Add(time.Hour)) + } + + return nil +} + +func enableSPN() { + err := config.SetConfigOption("spn/enable", true) + if err != nil { + log.Warningf("spn/access: failed to enable the SPN during login: %s", err) + } +} + +func disableSPN() { + err := config.SetConfigOption("spn/enable", false) + if err != nil { + log.Warningf("spn/access: failed to disable the SPN during logout: %s", err) + } +} + +// TokenIssuerIsFailing returns whether token issuing is currently failing. +func TokenIssuerIsFailing() bool { + return tokenIssuerIsFailing.IsSet() +} + +func tokenIssuerFailed() { + if !tokenIssuerIsFailing.SetToIf(false, true) { + return + } + if !module.Online() { + return + } + + accountUpdateTask.Schedule(time.Now().Add(tokenIssuerRetryDuration)) +} + +// IsLoggedIn returns whether a User is currently logged in. +func (user *UserRecord) IsLoggedIn() bool { + user.Lock() + defer user.Unlock() + + switch user.State { + case account.UserStateNone, account.UserStateLoggedOut: + return false + default: + return true + } +} + +// MayUseTheSPN returns whether the currently logged in User may use the SPN. +func (user *UserRecord) MayUseTheSPN() bool { + user.Lock() + defer user.Unlock() + + return user.User.MayUseSPN() +} diff --git a/spn/access/module_test.go b/spn/access/module_test.go new file mode 100644 index 00000000..59d69be6 --- /dev/null +++ b/spn/access/module_test.go @@ -0,0 +1,13 @@ +package access + +import ( + "testing" + + "github.com/safing/portmaster/service/core/pmtesting" + "github.com/safing/portmaster/spn/conf" +) + +func TestMain(m *testing.M) { + conf.EnableClient(true) + pmtesting.TestMain(m, module) +} diff --git a/spn/access/notify.go b/spn/access/notify.go new file mode 100644 index 00000000..978a2f16 --- /dev/null +++ b/spn/access/notify.go @@ -0,0 +1,105 @@ +package access + +import ( + "fmt" + "strings" + "time" + + "github.com/safing/portbase/log" + "github.com/safing/portbase/notifications" +) + +const ( + day = 24 * time.Hour + week = 7 * day + + endOfPackageNearNotifID = "access:end-of-package-near" +) + +func notifyOfPackageEnd(u *UserRecord) { + // TODO: Check if subscription auto-renews. + + // Skip if there is not active subscription or if it has ended already. + switch { + case u.Subscription == nil, // No subscription. + u.Subscription.EndsAt == nil, // Subscription not active. + u.Subscription.NextBillingDate != nil, // Subscription is auto-renewing. + time.Now().After(*u.Subscription.EndsAt): // Subscription has ended. + return + } + + // Calculate durations. + sinceLastNotified := 52 * week // Never. + if u.LastNotifiedOfEnd != nil { + sinceLastNotified = time.Since(*u.LastNotifiedOfEnd) + } + untilEnd := time.Until(*u.Subscription.EndsAt) + + // Notify every two days in the week before end. + notifType := notifications.Info + switch { + case untilEnd < week && sinceLastNotified > 2*day: + // Notify 7, 5, 3 and 1 days before end. + if untilEnd < 4*day { + notifType = notifications.Warning + } + fallthrough + + case u.CurrentPlan != nil && u.CurrentPlan.Months >= 6 && + untilEnd < 4*week && sinceLastNotified > week: + // Notify 4, 3 and 2 weeks before end - on long running packages. + + // Get names and messages. + packageNameTitle := "Portmaster Package" + if u.CurrentPlan != nil { + packageNameTitle = u.CurrentPlan.Name + } + packageNameBody := packageNameTitle + if !strings.HasSuffix(packageNameBody, " Package") { + packageNameBody += " Package" + } + + var endsText string + daysUntilEnd := untilEnd / day + switch daysUntilEnd { //nolint:exhaustive + case 0: + endsText = "today" + case 1: + endsText = "tomorrow" + default: + endsText = fmt.Sprintf("in %d days", daysUntilEnd) + } + + // Send notification. + notifications.Notify(¬ifications.Notification{ + EventID: endOfPackageNearNotifID, + Type: notifType, + Title: fmt.Sprintf("%s About to Expire", packageNameTitle), + Message: fmt.Sprintf( + "Your current %s ends %s. Extend it to keep your full privacy protections.", + packageNameBody, + endsText, + ), + ShowOnSystem: notifType == notifications.Warning, + AvailableActions: []*notifications.Action{ + { + Text: "Open Account Page", + Type: notifications.ActionTypeOpenURL, + Payload: "https://account.safing.io", + }, + { + ID: "ack", + Text: "Got it!", + }, + }, + }) + + // Save that we sent a notification. + now := time.Now() + u.LastNotifiedOfEnd = &now + err := u.Save() + if err != nil { + log.Warningf("spn/access: failed to save user after sending subscription ending soon notification: %s", err) + } + } +} diff --git a/spn/access/op_auth.go b/spn/access/op_auth.go new file mode 100644 index 00000000..764c73c3 --- /dev/null +++ b/spn/access/op_auth.go @@ -0,0 +1,75 @@ +package access + +import ( + "time" + + "github.com/safing/portbase/container" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/access/token" + "github.com/safing/portmaster/spn/terminal" +) + +// OpTypeAccessCodeAuth is the type ID of the auth operation. +const OpTypeAccessCodeAuth = "auth" + +func init() { + terminal.RegisterOpType(terminal.OperationFactory{ + Type: OpTypeAccessCodeAuth, + Start: checkAccessCode, + }) +} + +// AuthorizeOp is used to authorize a session. +type AuthorizeOp struct { + terminal.OneOffOperationBase +} + +// Type returns the type ID. +func (op *AuthorizeOp) Type() string { + return OpTypeAccessCodeAuth +} + +// AuthorizeToTerminal starts an authorization operation. +func AuthorizeToTerminal(t terminal.Terminal) (*AuthorizeOp, *terminal.Error) { + op := &AuthorizeOp{} + op.Init() + + newToken, err := GetToken(ExpandAndConnectZones) + if err != nil { + return nil, terminal.ErrInternalError.With("failed to get access token: %w", err) + } + + tErr := t.StartOperation(op, container.New(newToken.Raw()), 10*time.Second) + if tErr != nil { + return nil, terminal.ErrInternalError.With("failed to init auth op: %w", tErr) + } + + return op, nil +} + +func checkAccessCode(t terminal.Terminal, opID uint32, initData *container.Container) (terminal.Operation, *terminal.Error) { + // Parse provided access token. + receivedToken, err := token.ParseRawToken(initData.CompileData()) + if err != nil { + return nil, terminal.ErrMalformedData.With("failed to parse access token: %w", err) + } + + // Check if token is valid. + granted, err := VerifyToken(receivedToken) + if err != nil { + return nil, terminal.ErrPermissionDenied.With("invalid access token: %w", err) + } + + // Get the authorizing terminal for applying the granted permission. + authTerm, ok := t.(terminal.AuthorizingTerminal) + if !ok { + return nil, terminal.ErrIncorrectUsage.With("terminal does not handle authorization") + } + + // Grant permissions. + authTerm.GrantPermission(granted) + log.Debugf("spn/access: granted %s permissions via %s zone", t.FmtID(), receivedToken.Zone) + + // End successfully. + return nil, terminal.ErrExplicitAck +} diff --git a/spn/access/storage.go b/spn/access/storage.go new file mode 100644 index 00000000..fcbb7edc --- /dev/null +++ b/spn/access/storage.go @@ -0,0 +1,131 @@ +package access + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/safing/portbase/database" + "github.com/safing/portbase/database/query" + "github.com/safing/portbase/database/record" + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/access/token" +) + +func loadTokens() { + for _, zone := range persistentZones { + // Get handler of zone. + handler, ok := token.GetHandler(zone) + if !ok { + log.Warningf("spn/access: could not find zone %s for loading tokens", zone) + continue + } + + // Get data from database. + r, err := db.Get(fmt.Sprintf(tokenStorageKeyTemplate, zone)) + if err != nil { + if errors.Is(err, database.ErrNotFound) { + log.Debugf("spn/access: no %s tokens to load", zone) + } else { + log.Warningf("spn/access: failed to load %s tokens: %s", zone, err) + } + continue + } + + // Get wrapper. + wrapper, ok := r.(*record.Wrapper) + if !ok { + log.Warningf("spn/access: failed to parse %s tokens: expected wrapper, got %T", zone, r) + continue + } + + // Load into handler. + err = handler.Load(wrapper.Data) + if err != nil { + log.Warningf("spn/access: failed to load %s tokens: %s", zone, err) + } + log.Infof("spn/access: loaded %d %s tokens", handler.Amount(), zone) + } +} + +func storeTokens() { + for _, zone := range persistentZones { + // Get handler of zone. + handler, ok := token.GetHandler(zone) + if !ok { + log.Warningf("spn/access: could not find zone %s for storing tokens", zone) + continue + } + + // Generate storage key. + storageKey := fmt.Sprintf(tokenStorageKeyTemplate, zone) + + // Check if there is data to save. + amount := handler.Amount() + if amount == 0 { + // Remove possible old entry from database. + err := db.Delete(storageKey) + if err != nil { + log.Warningf("spn/access: failed to delete possible old %s tokens from storage: %s", zone, err) + } + log.Debugf("spn/access: no %s tokens to store", zone) + continue + } + + // Export data. + data, err := handler.Save() + if err != nil { + log.Warningf("spn/access: failed to export %s tokens for storing: %s", zone, err) + continue + } + + // Wrap data into raw record. + r, err := record.NewWrapper(storageKey, nil, dsd.RAW, data) + if err != nil { + log.Warningf("spn/access: failed to prepare %s token export for storing: %s", zone, err) + continue + } + + // Let tokens expire after one month. + // This will regularly happen when we switch zones. + r.UpdateMeta() + r.Meta().MakeSecret() + r.Meta().MakeCrownJewel() + r.Meta().SetRelativateExpiry(30 * 86400) + + // Save to database. + err = db.Put(r) + if err != nil { + log.Warningf("spn/access: failed to store %s tokens: %s", zone, err) + continue + } + + log.Infof("spn/access: stored %d %s tokens", amount, zone) + } +} + +func clearTokens() { + for _, zone := range persistentZones { + // Get handler of zone. + handler, ok := token.GetHandler(zone) + if !ok { + log.Warningf("spn/access: could not find zone %s for clearing tokens", zone) + continue + } + + // Clear tokens. + handler.Clear() + } + + // Purge database storage prefix. + ctx, cancel := context.WithTimeout(module.Ctx, 10*time.Second) + defer cancel() + n, err := db.Purge(ctx, query.New(fmt.Sprintf(tokenStorageKeyTemplate, ""))) + if err != nil { + log.Warningf("spn/access: failed to clear token storages: %s", err) + return + } + log.Infof("spn/access: cleared %d token storages", n) +} diff --git a/spn/access/token/errors.go b/spn/access/token/errors.go new file mode 100644 index 00000000..b19fbb28 --- /dev/null +++ b/spn/access/token/errors.go @@ -0,0 +1,15 @@ +package token + +import "errors" + +// Errors. +var ( + ErrEmpty = errors.New("token storage is empty") + ErrNoZone = errors.New("no zone specified") + ErrTokenInvalid = errors.New("token is invalid") + ErrTokenMalformed = errors.New("token malformed") + ErrTokenUsed = errors.New("token already used") + ErrZoneMismatch = errors.New("zone mismatch") + ErrZoneTaken = errors.New("zone taken") + ErrZoneUnknown = errors.New("zone unknown") +) diff --git a/spn/access/token/module_test.go b/spn/access/token/module_test.go new file mode 100644 index 00000000..bb79d76f --- /dev/null +++ b/spn/access/token/module_test.go @@ -0,0 +1,13 @@ +package token + +import ( + "testing" + + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/service/core/pmtesting" +) + +func TestMain(m *testing.M) { + module := modules.Register("token", nil, nil, nil, "rng") + pmtesting.TestMain(m, module) +} diff --git a/spn/access/token/pblind.go b/spn/access/token/pblind.go new file mode 100644 index 00000000..71f137a3 --- /dev/null +++ b/spn/access/token/pblind.go @@ -0,0 +1,552 @@ +package token + +import ( + "crypto/elliptic" + "crypto/rand" + "errors" + "fmt" + "math" + "math/big" + mrand "math/rand" + "sync" + + "github.com/mr-tron/base58" + "github.com/rot256/pblind" + + "github.com/safing/portbase/container" + "github.com/safing/portbase/formats/dsd" +) + +const pblindSecretSize = 32 + +// PBlindToken is token based on the pblind library. +type PBlindToken struct { + Serial int `json:"N,omitempty"` + Token []byte `json:"T,omitempty"` + Signature *pblind.Signature `json:"S,omitempty"` +} + +// Pack packs the token. +func (pbt *PBlindToken) Pack() ([]byte, error) { + return dsd.Dump(pbt, dsd.CBOR) +} + +// UnpackPBlindToken unpacks the token. +func UnpackPBlindToken(token []byte) (*PBlindToken, error) { + t := &PBlindToken{} + + _, err := dsd.Load(token, t) + if err != nil { + return nil, err + } + + return t, nil +} + +// PBlindHandler is a handler for the pblind tokens. +type PBlindHandler struct { + sync.Mutex + opts *PBlindOptions + + publicKey *pblind.PublicKey + privateKey *pblind.SecretKey + + storageLock sync.Mutex + Storage []*PBlindToken + + // Client request state. + requestStateLock sync.Mutex + requestState []RequestState +} + +// PBlindOptions are options for the PBlindHandler. +type PBlindOptions struct { + Zone string + CurveName string + Curve elliptic.Curve + PublicKey string + PrivateKey string + BatchSize int + UseSerials bool + RandomizeOrder bool + Fallback bool + SignalShouldRequest func(Handler) + DoubleSpendProtection func([]byte) error +} + +// PBlindSignerState is a signer state. +type PBlindSignerState struct { + signers []*pblind.StateSigner +} + +// PBlindSetupResponse is a setup response. +type PBlindSetupResponse struct { + Msgs []*pblind.Message1 +} + +// PBlindTokenRequest is a token request. +type PBlindTokenRequest struct { + Msgs []*pblind.Message2 +} + +// IssuedPBlindTokens are issued pblind tokens. +type IssuedPBlindTokens struct { + Msgs []*pblind.Message3 +} + +// RequestState is a request state. +type RequestState struct { + Token []byte + State *pblind.StateRequester +} + +// NewPBlindHandler creates a new pblind handler. +func NewPBlindHandler(opts PBlindOptions) (*PBlindHandler, error) { + pbh := &PBlindHandler{ + opts: &opts, + } + + // Check curve, get from name. + if opts.Curve == nil { + switch opts.CurveName { + case "P-256": + opts.Curve = elliptic.P256() + case "P-384": + opts.Curve = elliptic.P384() + case "P-521": + opts.Curve = elliptic.P521() + default: + return nil, errors.New("no curve supplied") + } + } else if opts.CurveName != "" { + return nil, errors.New("both curve and curve name supplied") + } + + // Load keys. + switch { + case pbh.opts.PrivateKey != "": + keyData, err := base58.Decode(pbh.opts.PrivateKey) + if err != nil { + return nil, fmt.Errorf("failed to decode private key: %w", err) + } + pivateKey := pblind.SecretKeyFromBytes(pbh.opts.Curve, keyData) + pbh.privateKey = &pivateKey + publicKey := pbh.privateKey.GetPublicKey() + pbh.publicKey = &publicKey + + // Check public key if also provided. + if pbh.opts.PublicKey != "" { + if pbh.opts.PublicKey != base58.Encode(pbh.publicKey.Bytes()) { + return nil, errors.New("private and public mismatch") + } + } + + case pbh.opts.PublicKey != "": + keyData, err := base58.Decode(pbh.opts.PublicKey) + if err != nil { + return nil, fmt.Errorf("failed to decode public key: %w", err) + } + publicKey, err := pblind.PublicKeyFromBytes(pbh.opts.Curve, keyData) + if err != nil { + return nil, fmt.Errorf("failed to decode public key: %w", err) + } + pbh.publicKey = &publicKey + + default: + return nil, errors.New("no key supplied") + } + + return pbh, nil +} + +func (pbh *PBlindHandler) makeInfo(serial int) (*pblind.Info, error) { + // Gather data for info. + infoData := container.New() + infoData.AppendAsBlock([]byte(pbh.opts.Zone)) + if pbh.opts.UseSerials { + infoData.AppendInt(serial) + } + + // Compress to point. + info, err := pblind.CompressInfo(pbh.opts.Curve, infoData.CompileData()) + if err != nil { + return nil, fmt.Errorf("failed to compress info: %w", err) + } + + return &info, nil +} + +// Zone returns the zone name. +func (pbh *PBlindHandler) Zone() string { + return pbh.opts.Zone +} + +// ShouldRequest returns whether the new tokens should be requested. +func (pbh *PBlindHandler) ShouldRequest() bool { + pbh.storageLock.Lock() + defer pbh.storageLock.Unlock() + + return pbh.shouldRequest() +} + +func (pbh *PBlindHandler) shouldRequest() bool { + // Return true if storage is at or below 10%. + return len(pbh.Storage) == 0 || pbh.opts.BatchSize/len(pbh.Storage) > 10 +} + +// Amount returns the current amount of tokens in this handler. +func (pbh *PBlindHandler) Amount() int { + pbh.storageLock.Lock() + defer pbh.storageLock.Unlock() + + return len(pbh.Storage) +} + +// IsFallback returns whether this handler should only be used as a fallback. +func (pbh *PBlindHandler) IsFallback() bool { + return pbh.opts.Fallback +} + +// CreateSetup sets up signers for a request. +func (pbh *PBlindHandler) CreateSetup() (state *PBlindSignerState, setupResponse *PBlindSetupResponse, err error) { + state = &PBlindSignerState{ + signers: make([]*pblind.StateSigner, pbh.opts.BatchSize), + } + setupResponse = &PBlindSetupResponse{ + Msgs: make([]*pblind.Message1, pbh.opts.BatchSize), + } + + // Go through the batch. + for i := 0; i < pbh.opts.BatchSize; i++ { + info, err := pbh.makeInfo(i + 1) + if err != nil { + return nil, nil, fmt.Errorf("failed to create info #%d: %w", i, err) + } + + // Create signer. + signer, err := pblind.CreateSigner(*pbh.privateKey, *info) + if err != nil { + return nil, nil, fmt.Errorf("failed to create signer #%d: %w", i, err) + } + state.signers[i] = signer + + // Create request setup. + setupMsg, err := signer.CreateMessage1() + if err != nil { + return nil, nil, fmt.Errorf("failed to create setup msg #%d: %w", i, err) + } + setupResponse.Msgs[i] = &setupMsg + } + + return state, setupResponse, nil +} + +// CreateTokenRequest creates a token request to be sent to the token server. +func (pbh *PBlindHandler) CreateTokenRequest(requestSetup *PBlindSetupResponse) (request *PBlindTokenRequest, err error) { + // Check request setup data. + if len(requestSetup.Msgs) != pbh.opts.BatchSize { + return nil, fmt.Errorf("invalid request setup msg count of %d", len(requestSetup.Msgs)) + } + + // Lock and reset the request state. + pbh.requestStateLock.Lock() + defer pbh.requestStateLock.Unlock() + pbh.requestState = make([]RequestState, pbh.opts.BatchSize) + request = &PBlindTokenRequest{ + Msgs: make([]*pblind.Message2, pbh.opts.BatchSize), + } + + // Go through the batch. + for i := 0; i < pbh.opts.BatchSize; i++ { + // Check if we have setup data. + if requestSetup.Msgs[i] == nil { + return nil, fmt.Errorf("missing setup data #%d", i) + } + + // Generate secret token. + token := make([]byte, pblindSecretSize) + n, err := rand.Read(token) //nolint:gosec // False positive - check the imports. + if err != nil { + return nil, fmt.Errorf("failed to get random token #%d: %w", i, err) + } + if n != pblindSecretSize { + return nil, fmt.Errorf("failed to get full random token #%d: only got %d bytes", i, n) + } + pbh.requestState[i].Token = token + + // Create public metadata. + info, err := pbh.makeInfo(i + 1) + if err != nil { + return nil, fmt.Errorf("failed to make token info #%d: %w", i, err) + } + + // Create request and request state. + requester, err := pblind.CreateRequester(*pbh.publicKey, *info, token) + if err != nil { + return nil, fmt.Errorf("failed to create request state #%d: %w", i, err) + } + pbh.requestState[i].State = requester + + err = requester.ProcessMessage1(*requestSetup.Msgs[i]) + if err != nil { + return nil, fmt.Errorf("failed to process setup message #%d: %w", i, err) + } + + // Create request message. + requestMsg, err := requester.CreateMessage2() + if err != nil { + return nil, fmt.Errorf("failed to create request message #%d: %w", i, err) + } + request.Msgs[i] = &requestMsg + } + + return request, nil +} + +// IssueTokens sign the requested tokens. +func (pbh *PBlindHandler) IssueTokens(state *PBlindSignerState, request *PBlindTokenRequest) (response *IssuedPBlindTokens, err error) { + // Check request data. + if len(request.Msgs) != pbh.opts.BatchSize { + return nil, fmt.Errorf("invalid request msg count of %d", len(request.Msgs)) + } + if len(state.signers) != pbh.opts.BatchSize { + return nil, fmt.Errorf("invalid request state count of %d", len(request.Msgs)) + } + + // Create response. + response = &IssuedPBlindTokens{ + Msgs: make([]*pblind.Message3, pbh.opts.BatchSize), + } + + // Go through the batch. + for i := 0; i < pbh.opts.BatchSize; i++ { + // Check if we have request data. + if request.Msgs[i] == nil { + return nil, fmt.Errorf("missing request data #%d", i) + } + + // Process request msg. + err = state.signers[i].ProcessMessage2(*request.Msgs[i]) + if err != nil { + return nil, fmt.Errorf("failed to process request msg #%d: %w", i, err) + } + + // Issue token. + responseMsg, err := state.signers[i].CreateMessage3() + if err != nil { + return nil, fmt.Errorf("failed to issue token #%d: %w", i, err) + } + response.Msgs[i] = &responseMsg + } + + return response, nil +} + +// ProcessIssuedTokens processes the issued token from the server. +func (pbh *PBlindHandler) ProcessIssuedTokens(issuedTokens *IssuedPBlindTokens) error { + // Check data. + if len(issuedTokens.Msgs) != pbh.opts.BatchSize { + return fmt.Errorf("invalid issued token count of %d", len(issuedTokens.Msgs)) + } + + // Step 1: Process issued tokens. + + // Lock and reset the request state. + pbh.requestStateLock.Lock() + defer pbh.requestStateLock.Unlock() + defer func() { + pbh.requestState = make([]RequestState, pbh.opts.BatchSize) + }() + finalizedTokens := make([]*PBlindToken, pbh.opts.BatchSize) + + // Go through the batch. + for i := 0; i < pbh.opts.BatchSize; i++ { + // Finalize token. + err := pbh.requestState[i].State.ProcessMessage3(*issuedTokens.Msgs[i]) + if err != nil { + return fmt.Errorf("failed to create final signature #%d: %w", i, err) + } + + // Get and check final signature. + signature, err := pbh.requestState[i].State.Signature() + if err != nil { + return fmt.Errorf("failed to create final signature #%d: %w", i, err) + } + info, err := pbh.makeInfo(i + 1) + if err != nil { + return fmt.Errorf("failed to make token info #%d: %w", i, err) + } + if !pbh.publicKey.Check(signature, *info, pbh.requestState[i].Token) { + return fmt.Errorf("invalid signature on #%d", i) + } + + // Save to temporary slice. + newToken := &PBlindToken{ + Token: pbh.requestState[i].Token, + Signature: &signature, + } + if pbh.opts.UseSerials { + newToken.Serial = i + 1 + } + finalizedTokens[i] = newToken + } + + // Step 2: Randomize received tokens + + if pbh.opts.RandomizeOrder { + rInt, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + return fmt.Errorf("failed to get seed for shuffle: %w", err) + } + mr := mrand.New(mrand.NewSource(rInt.Int64())) //nolint:gosec + mr.Shuffle(len(finalizedTokens), func(i, j int) { + finalizedTokens[i], finalizedTokens[j] = finalizedTokens[j], finalizedTokens[i] + }) + } + + // Step 3: Add tokens to storage. + + // Wait for all processing to be complete, as using tokens from a faulty + // batch can be dangerous, as the server could be doing this purposely to + // create conditions that may benefit an attacker. + + pbh.storageLock.Lock() + defer pbh.storageLock.Unlock() + + // Add finalized tokens to storage. + pbh.Storage = append(pbh.Storage, finalizedTokens...) + + return nil +} + +// GetToken returns a token. +func (pbh *PBlindHandler) GetToken() (token *Token, err error) { + pbh.storageLock.Lock() + defer pbh.storageLock.Unlock() + + // Check if we have supply. + if len(pbh.Storage) == 0 { + return nil, ErrEmpty + } + + // Pack token. + data, err := pbh.Storage[0].Pack() + if err != nil { + return nil, fmt.Errorf("failed to pack token: %w", err) + } + + // Shift to next token. + pbh.Storage = pbh.Storage[1:] + + // Check if we should signal that we should request tokens. + if pbh.opts.SignalShouldRequest != nil && pbh.shouldRequest() { + pbh.opts.SignalShouldRequest(pbh) + } + + return &Token{ + Zone: pbh.opts.Zone, + Data: data, + }, nil +} + +// Verify verifies the given token. +func (pbh *PBlindHandler) Verify(token *Token) error { + // Check if zone matches. + if token.Zone != pbh.opts.Zone { + return ErrZoneMismatch + } + + // Unpack token. + t, err := UnpackPBlindToken(token.Data) + if err != nil { + return fmt.Errorf("%w: %w", ErrTokenMalformed, err) + } + + // Check if serial is valid. + switch { + case pbh.opts.UseSerials && t.Serial > 0 && t.Serial <= pbh.opts.BatchSize: + // Using serials in accepted range. + case !pbh.opts.UseSerials && t.Serial == 0: + // Not using serials and serial is zero. + default: + return fmt.Errorf("%w: invalid serial", ErrTokenMalformed) + } + + // Build info for checking signature. + info, err := pbh.makeInfo(t.Serial) + if err != nil { + return fmt.Errorf("%w: %w", ErrTokenMalformed, err) + } + + // Check signature. + if !pbh.publicKey.Check(*t.Signature, *info, t.Token) { + return ErrTokenInvalid + } + + // Check for double spending. + if pbh.opts.DoubleSpendProtection != nil { + if err := pbh.opts.DoubleSpendProtection(t.Token); err != nil { + return fmt.Errorf("%w: %w", ErrTokenUsed, err) + } + } + + return nil +} + +// PBlindStorage is a storage for pblind tokens. +type PBlindStorage struct { + Storage []*PBlindToken +} + +// Save serializes and returns the current tokens. +func (pbh *PBlindHandler) Save() ([]byte, error) { + pbh.storageLock.Lock() + defer pbh.storageLock.Unlock() + + if len(pbh.Storage) == 0 { + return nil, ErrEmpty + } + + s := &PBlindStorage{ + Storage: pbh.Storage, + } + + return dsd.Dump(s, dsd.CBOR) +} + +// Load loads the given tokens into the handler. +func (pbh *PBlindHandler) Load(data []byte) error { + pbh.storageLock.Lock() + defer pbh.storageLock.Unlock() + + s := &PBlindStorage{} + _, err := dsd.Load(data, s) + if err != nil { + return err + } + + // Check signatures on load. + for _, t := range s.Storage { + // Build info for checking signature. + info, err := pbh.makeInfo(t.Serial) + if err != nil { + return err + } + + // Check signature. + if !pbh.publicKey.Check(*t.Signature, *info, t.Token) { + return ErrTokenInvalid + } + } + + pbh.Storage = s.Storage + return nil +} + +// Clear clears all the tokens in the handler. +func (pbh *PBlindHandler) Clear() { + pbh.storageLock.Lock() + defer pbh.storageLock.Unlock() + + pbh.Storage = nil +} diff --git a/spn/access/token/pblind_gen_test.go b/spn/access/token/pblind_gen_test.go new file mode 100644 index 00000000..416213ae --- /dev/null +++ b/spn/access/token/pblind_gen_test.go @@ -0,0 +1,39 @@ +package token + +import ( + "crypto/elliptic" + "fmt" + "testing" + + "github.com/mr-tron/base58" + "github.com/rot256/pblind" +) + +func TestGeneratePBlindKeys(t *testing.T) { + t.Parallel() + + for _, curve := range []elliptic.Curve{ + elliptic.P256(), + elliptic.P384(), + elliptic.P521(), + } { + privateKey, err := pblind.NewSecretKey(curve) + if err != nil { + t.Fatal(err) + } + publicKey := privateKey.GetPublicKey() + + fmt.Printf( + "%s (%dbit) private key: %s\n", + curve.Params().Name, + curve.Params().BitSize, + base58.Encode(privateKey.Bytes()), + ) + fmt.Printf( + "%s (%dbit) public key: %s\n", + curve.Params().Name, + curve.Params().BitSize, + base58.Encode(publicKey.Bytes()), + ) + } +} diff --git a/spn/access/token/pblind_test.go b/spn/access/token/pblind_test.go new file mode 100644 index 00000000..b25ac71b --- /dev/null +++ b/spn/access/token/pblind_test.go @@ -0,0 +1,260 @@ +package token + +import ( + "crypto/elliptic" + "encoding/asn1" + "testing" + "time" + + "github.com/rot256/pblind" +) + +const PBlindTestZone = "test-pblind" + +func init() { + // Combined testing config. + + h, err := NewPBlindHandler(PBlindOptions{ + Zone: PBlindTestZone, + Curve: elliptic.P256(), + PrivateKey: "HbwGtLsqek1Fdwuz1MhNQfiY7tj9EpWHeMWHPZ9c6KYY", + UseSerials: true, + BatchSize: 1000, + RandomizeOrder: true, + }) + if err != nil { + panic(err) + } + + err = RegisterPBlindHandler(h) + if err != nil { + panic(err) + } +} + +func TestPBlind(t *testing.T) { + t.Parallel() + + opts := &PBlindOptions{ + Zone: PBlindTestZone, + Curve: elliptic.P256(), + UseSerials: true, + BatchSize: 1000, + RandomizeOrder: true, + } + + // Issuer + opts.PrivateKey = "HbwGtLsqek1Fdwuz1MhNQfiY7tj9EpWHeMWHPZ9c6KYY" + issuer, err := NewPBlindHandler(*opts) + if err != nil { + t.Fatal(err) + } + + // Client + opts.PrivateKey = "" + opts.PublicKey = "285oMDh3w5mxyFgpmmURifKfhkcqwwsdnePpPZ6Nqm8cc" + client, err := NewPBlindHandler(*opts) + if err != nil { + t.Fatal(err) + } + + // Verifier + verifier, err := NewPBlindHandler(*opts) + if err != nil { + t.Fatal(err) + } + + // Play through the whole use case. + + signerState, setupResponse, err := issuer.CreateSetup() + if err != nil { + t.Fatal(err) + } + + request, err := client.CreateTokenRequest(setupResponse) + if err != nil { + t.Fatal(err) + } + + issuedTokens, err := issuer.IssueTokens(signerState, request) + if err != nil { + t.Fatal(err) + } + + err = client.ProcessIssuedTokens(issuedTokens) + if err != nil { + t.Fatal(err) + } + + token, err := client.GetToken() + if err != nil { + t.Fatal(err) + } + + err = verifier.Verify(token) + if err != nil { + t.Fatal(err) + } +} + +func TestPBlindLibrary(t *testing.T) { + t.Parallel() + + // generate a key-pair + + curve := elliptic.P256() + + sk, _ := pblind.NewSecretKey(curve) + pk := sk.GetPublicKey() + + msgStr := []byte("128b_accesstoken") + infoStr := []byte("v=1 serial=12345") + info, err := pblind.CompressInfo(curve, infoStr) + if err != nil { + t.Fatal(err) + } + + totalStart := time.Now() + batchSize := 1000 + + signers := make([]*pblind.StateSigner, batchSize) + requesters := make([]*pblind.StateRequester, batchSize) + toServer := make([][]byte, batchSize) + toClient := make([][]byte, batchSize) + + // Create signers and prep requests. + start := time.Now() + for i := 0; i < batchSize; i++ { + signer, err := pblind.CreateSigner(sk, info) + if err != nil { + t.Fatal(err) + } + signers[i] = signer + + msg1S, err := signer.CreateMessage1() + if err != nil { + t.Fatal(err) + } + ser1S, err := asn1.Marshal(msg1S) + if err != nil { + t.Fatal(err) + } + toClient[i] = ser1S + } + t.Logf("created %d signers and request preps in %s", batchSize, time.Since(start)) + t.Logf("sending %d bytes to client", lenOfByteSlices(toClient)) + + // Create requesters and create requests. + start = time.Now() + for i := 0; i < batchSize; i++ { + requester, err := pblind.CreateRequester(pk, info, msgStr) + if err != nil { + t.Fatal(err) + } + requesters[i] = requester + + var msg1R pblind.Message1 + _, err = asn1.Unmarshal(toClient[i], &msg1R) + if err != nil { + t.Fatal(err) + } + err = requester.ProcessMessage1(msg1R) + if err != nil { + t.Fatal(err) + } + + msg2R, err := requester.CreateMessage2() + if err != nil { + t.Fatal(err) + } + ser2R, err := asn1.Marshal(msg2R) + if err != nil { + t.Fatal(err) + } + toServer[i] = ser2R + } + t.Logf("created %d requesters and requests in %s", batchSize, time.Since(start)) + t.Logf("sending %d bytes to server", lenOfByteSlices(toServer)) + + // Sign requests + start = time.Now() + for i := 0; i < batchSize; i++ { + var msg2S pblind.Message2 + _, err = asn1.Unmarshal(toServer[i], &msg2S) + if err != nil { + t.Fatal(err) + } + err = signers[i].ProcessMessage2(msg2S) + if err != nil { + t.Fatal(err) + } + + msg3S, err := signers[i].CreateMessage3() + if err != nil { + t.Fatal(err) + } + ser3S, err := asn1.Marshal(msg3S) + if err != nil { + t.Fatal(err) + } + toClient[i] = ser3S + } + t.Logf("signed %d requests in %s", batchSize, time.Since(start)) + t.Logf("sending %d bytes to client", lenOfByteSlices(toClient)) + + // Verify signed requests + start = time.Now() + for i := 0; i < batchSize; i++ { + var msg3R pblind.Message3 + _, err := asn1.Unmarshal(toClient[i], &msg3R) + if err != nil { + t.Fatal(err) + } + err = requesters[i].ProcessMessage3(msg3R) + if err != nil { + t.Fatal(err) + } + signature, err := requesters[i].Signature() + if err != nil { + t.Fatal(err) + } + sig, err := asn1.Marshal(signature) + if err != nil { + t.Fatal(err) + } + toServer[i] = sig + + // check signature + if !pk.Check(signature, info, msgStr) { + t.Fatal("signature invalid") + } + } + t.Logf("finalized and verified %d signed tokens in %s", batchSize, time.Since(start)) + t.Logf("stored %d signed tokens in %d bytes", batchSize, lenOfByteSlices(toServer)) + + // Verify on server + start = time.Now() + for i := 0; i < batchSize; i++ { + var sig pblind.Signature + _, err := asn1.Unmarshal(toServer[i], &sig) + if err != nil { + t.Fatal(err) + } + + // check signature + if !pk.Check(sig, info, msgStr) { + t.Fatal("signature invalid") + } + } + t.Logf("verified %d signed tokens in %s", batchSize, time.Since(start)) + + t.Logf("process complete") + t.Logf("simulated the whole process for %d tokens in %s", batchSize, time.Since(totalStart)) +} + +func lenOfByteSlices(v [][]byte) (length int) { + for _, s := range v { + length += len(s) + } + return +} diff --git a/spn/access/token/registry.go b/spn/access/token/registry.go new file mode 100644 index 00000000..d20ec6f0 --- /dev/null +++ b/spn/access/token/registry.go @@ -0,0 +1,116 @@ +package token + +import "sync" + +// Handler represents a token handling system. +type Handler interface { + // Zone returns the zone name. + Zone() string + + // ShouldRequest returns whether the new tokens should be requested. + ShouldRequest() bool + + // Amount returns the current amount of tokens in this handler. + Amount() int + + // IsFallback returns whether this handler should only be used as a fallback. + IsFallback() bool + + // GetToken returns a token. + GetToken() (token *Token, err error) + + // Verify verifies the given token. + Verify(token *Token) error + + // Save serializes and returns the current tokens. + Save() ([]byte, error) + + // Load loads the given tokens into the handler. + Load(data []byte) error + + // Clear clears all the tokens in the handler. + Clear() +} + +var ( + registry map[string]Handler + pblindRegistry []*PBlindHandler + scrambleRegistry []*ScrambleHandler + + registryLock sync.RWMutex +) + +func init() { + initRegistry() +} + +func initRegistry() { + registry = make(map[string]Handler) + pblindRegistry = make([]*PBlindHandler, 0, 1) + scrambleRegistry = make([]*ScrambleHandler, 0, 1) +} + +// RegisterPBlindHandler registers a pblind handler with the registry. +func RegisterPBlindHandler(h *PBlindHandler) error { + registryLock.Lock() + defer registryLock.Unlock() + + if err := registerHandler(h, h.opts.Zone); err != nil { + return err + } + + pblindRegistry = append(pblindRegistry, h) + return nil +} + +// RegisterScrambleHandler registers a scramble handler with the registry. +func RegisterScrambleHandler(h *ScrambleHandler) error { + registryLock.Lock() + defer registryLock.Unlock() + + if err := registerHandler(h, h.opts.Zone); err != nil { + return err + } + + scrambleRegistry = append(scrambleRegistry, h) + return nil +} + +func registerHandler(h Handler, zone string) error { + if zone == "" { + return ErrNoZone + } + + _, ok := registry[zone] + if ok { + return ErrZoneTaken + } + + registry[zone] = h + return nil +} + +// GetHandler returns the handler of the given zone. +func GetHandler(zone string) (handler Handler, ok bool) { + registryLock.RLock() + defer registryLock.RUnlock() + + handler, ok = registry[zone] + return +} + +// ResetRegistry resets the token handler registry. +func ResetRegistry() { + registryLock.Lock() + defer registryLock.Unlock() + + initRegistry() +} + +// RegistrySize returns the amount of handler registered. +func RegistrySize() int { + registryLock.Lock() + defer registryLock.Unlock() + + return len(registry) +} diff --git a/spn/access/token/request.go b/spn/access/token/request.go new file mode 100644 index 00000000..70e9422a --- /dev/null +++ b/spn/access/token/request.go @@ -0,0 +1,244 @@ +package token + +import ( + "crypto/rand" + "errors" + "fmt" + + "github.com/mr-tron/base58" +) + +const sessionIDSize = 32 + +// RequestHandlingState is a request handling state. +type RequestHandlingState struct { + SessionID string + PBlind map[string]*PBlindSignerState +} + +// SetupRequest is a setup request. +type SetupRequest struct { + PBlind map[string]struct{} `json:"PB,omitempty"` +} + +// SetupResponse is a setup response. +type SetupResponse struct { + SessionID string `json:"ID,omitempty"` + PBlind map[string]*PBlindSetupResponse `json:"PB,omitempty"` +} + +// TokenRequest is a token request. +type TokenRequest struct { //nolint:golint // Be explicit. + SessionID string `json:"ID,omitempty"` + PBlind map[string]*PBlindTokenRequest `json:"PB,omitempty"` + Scramble map[string]*ScrambleTokenRequest `json:"S,omitempty"` +} + +// IssuedTokens are issued tokens. +type IssuedTokens struct { + PBlind map[string]*IssuedPBlindTokens `json:"PB,omitempty"` + Scramble map[string]*IssuedScrambleTokens `json:"SC,omitempty"` +} + +// CreateSetupRequest creates a combined setup request for all registered tokens, if needed. +func CreateSetupRequest() (request *SetupRequest, setupRequired bool) { + registryLock.RLock() + defer registryLock.RUnlock() + + request = &SetupRequest{ + PBlind: make(map[string]struct{}, len(pblindRegistry)), + } + + // Go through handlers and create request setups. + for _, pblindHandler := range pblindRegistry { + // Check if we need to request with this handler. + if pblindHandler.ShouldRequest() { + request.PBlind[pblindHandler.Zone()] = struct{}{} + setupRequired = true + } + } + + return +} + +// HandleSetupRequest handles a setup request for all registered tokens. +func HandleSetupRequest(request *SetupRequest) (*RequestHandlingState, *SetupResponse, error) { + registryLock.RLock() + defer registryLock.RUnlock() + + // Generate session token. + randomID := make([]byte, sessionIDSize) + n, err := rand.Read(randomID) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate session ID: %w", err) + } + if n != sessionIDSize { + return nil, nil, fmt.Errorf("failed to get full session ID: only got %d bytes", n) + } + sessionID := base58.Encode(randomID) + + // Create state and response. + state := &RequestHandlingState{ + SessionID: sessionID, + PBlind: make(map[string]*PBlindSignerState, len(pblindRegistry)), + } + setup := &SetupResponse{ + SessionID: sessionID, + PBlind: make(map[string]*PBlindSetupResponse, len(pblindRegistry)), + } + + // Go through handlers and create setups. + for _, pblindHandler := range pblindRegistry { + // Check if we have a request for this handler. + _, ok := request.PBlind[pblindHandler.Zone()] + if !ok { + continue + } + + plindState, pblindSetup, err := pblindHandler.CreateSetup() + if err != nil { + return nil, nil, fmt.Errorf("failed to create setup for %s: %w", pblindHandler.Zone(), err) + } + + state.PBlind[pblindHandler.Zone()] = plindState + setup.PBlind[pblindHandler.Zone()] = pblindSetup + } + + return state, setup, nil +} + +// CreateTokenRequest creates a token request for all registered tokens. +func CreateTokenRequest(setup *SetupResponse) (request *TokenRequest, requestRequired bool, err error) { + registryLock.RLock() + defer registryLock.RUnlock() + + // Check setup data. + if setup != nil && setup.SessionID == "" { + return nil, false, errors.New("setup data is missing a session ID") + } + + // Create token request. + request = &TokenRequest{ + PBlind: make(map[string]*PBlindTokenRequest, len(pblindRegistry)), + Scramble: make(map[string]*ScrambleTokenRequest, len(scrambleRegistry)), + } + if setup != nil { + request.SessionID = setup.SessionID + } + + // Go through handlers and create requests. + if setup != nil { + for _, pblindHandler := range pblindRegistry { + // Check if we have setup data for this handler. + pblindSetup, ok := setup.PBlind[pblindHandler.Zone()] + if !ok { + // TODO: Abort if we should have received request data. + continue + } + + // Create request. + pblindRequest, err := pblindHandler.CreateTokenRequest(pblindSetup) + if err != nil { + return nil, false, fmt.Errorf("failed to create token request for %s: %w", pblindHandler.Zone(), err) + } + + requestRequired = true + request.PBlind[pblindHandler.Zone()] = pblindRequest + } + } + for _, scrambleHandler := range scrambleRegistry { + // Check if we need to request with this handler. + if scrambleHandler.ShouldRequest() { + requestRequired = true + request.Scramble[scrambleHandler.Zone()] = scrambleHandler.CreateTokenRequest() + } + } + + return request, requestRequired, nil +} + +// IssueTokens issues tokens for all registered tokens. +func IssueTokens(state *RequestHandlingState, request *TokenRequest) (response *IssuedTokens, err error) { + registryLock.RLock() + defer registryLock.RUnlock() + + // Create token response. + response = &IssuedTokens{ + PBlind: make(map[string]*IssuedPBlindTokens, len(pblindRegistry)), + Scramble: make(map[string]*IssuedScrambleTokens, len(scrambleRegistry)), + } + + // Go through handlers and create requests. + for _, pblindHandler := range pblindRegistry { + // Check if we have all the data for issuing. + pblindState, ok := state.PBlind[pblindHandler.Zone()] + if !ok { + continue + } + pblindRequest, ok := request.PBlind[pblindHandler.Zone()] + if !ok { + continue + } + + // Issue tokens. + pblindTokens, err := pblindHandler.IssueTokens(pblindState, pblindRequest) + if err != nil { + return nil, fmt.Errorf("failed to issue tokens for %s: %w", pblindHandler.Zone(), err) + } + + response.PBlind[pblindHandler.Zone()] = pblindTokens + } + for _, scrambleHandler := range scrambleRegistry { + // Check if we have all the data for issuing. + scrambleRequest, ok := request.Scramble[scrambleHandler.Zone()] + if !ok { + continue + } + + // Issue tokens. + scrambleTokens, err := scrambleHandler.IssueTokens(scrambleRequest) + if err != nil { + return nil, fmt.Errorf("failed to issue tokens for %s: %w", scrambleHandler.Zone(), err) + } + + response.Scramble[scrambleHandler.Zone()] = scrambleTokens + } + + return response, nil +} + +// ProcessIssuedTokens processes issued tokens for all registered tokens. +func ProcessIssuedTokens(response *IssuedTokens) error { + registryLock.RLock() + defer registryLock.RUnlock() + + // Go through handlers and create requests. + for _, pblindHandler := range pblindRegistry { + // Check if we received tokens. + pblindResponse, ok := response.PBlind[pblindHandler.Zone()] + if !ok { + continue + } + + // Process issued tokens. + err := pblindHandler.ProcessIssuedTokens(pblindResponse) + if err != nil { + return fmt.Errorf("failed to process issued tokens for %s: %w", pblindHandler.Zone(), err) + } + } + for _, scrambleHandler := range scrambleRegistry { + // Check if we received tokens. + scrambleResponse, ok := response.Scramble[scrambleHandler.Zone()] + if !ok { + continue + } + + // Process issued tokens. + err := scrambleHandler.ProcessIssuedTokens(scrambleResponse) + if err != nil { + return fmt.Errorf("failed to process issued tokens for %s: %w", scrambleHandler.Zone(), err) + } + } + + return nil +} diff --git a/spn/access/token/request_test.go b/spn/access/token/request_test.go new file mode 100644 index 00000000..7040672a --- /dev/null +++ b/spn/access/token/request_test.go @@ -0,0 +1,125 @@ +package token + +import ( + "testing" + "time" + + "github.com/safing/portbase/formats/dsd" +) + +func TestFull(t *testing.T) { + t.Parallel() + + testStart := time.Now() + + // Roundtrip 1 + + start := time.Now() + setupRequest, setupRequired := CreateSetupRequest() + if !setupRequired { + t.Fatal("setup should be required") + } + setupRequestData, err := dsd.Dump(setupRequest, dsd.CBOR) + if err != nil { + t.Fatal(err) + } + setupRequest = nil // nolint:ineffassign,wastedassign // Just to be sure. + t.Logf("setupRequest: %s, %d bytes", time.Since(start), len(setupRequestData)) + + start = time.Now() + loadedSetupRequest := &SetupRequest{} + _, err = dsd.Load(setupRequestData, loadedSetupRequest) + if err != nil { + t.Fatal(err) + } + serverState, setupResponse, err := HandleSetupRequest(loadedSetupRequest) + if err != nil { + t.Fatal(err) + } + setupResponseData, err := dsd.Dump(setupResponse, dsd.CBOR) + if err != nil { + t.Fatal(err) + } + setupResponse = nil // nolint:ineffassign,wastedassign // Just to be sure. + t.Logf("setupResponse: %s, %d bytes", time.Since(start), len(setupResponseData)) + + // Roundtrip 2 + + start = time.Now() + loadedSetupResponse := &SetupResponse{} + _, err = dsd.Load(setupResponseData, loadedSetupResponse) + if err != nil { + t.Fatal(err) + } + request, requestRequired, err := CreateTokenRequest(loadedSetupResponse) + if err != nil { + t.Fatal(err) + } + if !requestRequired { + t.Fatal("request should be required") + } + requestData, err := dsd.Dump(request, dsd.CBOR) + if err != nil { + t.Fatal(err) + } + request = nil // nolint:ineffassign,wastedassign // Just to be sure. + t.Logf("request: %s, %d bytes", time.Since(start), len(requestData)) + + start = time.Now() + loadedRequest := &TokenRequest{} + _, err = dsd.Load(requestData, loadedRequest) + if err != nil { + t.Fatal(err) + } + response, err := IssueTokens(serverState, loadedRequest) + if err != nil { + t.Fatal(err) + } + responseData, err := dsd.Dump(response, dsd.CBOR) + if err != nil { + t.Fatal(err) + } + response = nil // nolint:ineffassign,wastedassign // Just to be sure. + t.Logf("response: %s, %d bytes", time.Since(start), len(responseData)) + + start = time.Now() + loadedResponse := &IssuedTokens{} + _, err = dsd.Load(responseData, loadedResponse) + if err != nil { + t.Fatal(err) + } + err = ProcessIssuedTokens(loadedResponse) + if err != nil { + t.Fatal(err) + } + t.Logf("processing: %s", time.Since(start)) + + // Token Usage + + for _, testZone := range []string{ + PBlindTestZone, + ScrambleTestZone, + } { + start = time.Now() + + token, err := GetToken(testZone) + if err != nil { + t.Fatal(err) + } + tokenData := token.Raw() + token = nil // nolint:wastedassign // Just to be sure. + + loadedToken, err := ParseRawToken(tokenData) + if err != nil { + t.Fatal(err) + } + err = VerifyToken(loadedToken) + if err != nil { + t.Fatal(err) + } + + t.Logf("using %s token: %s", testZone, time.Since(start)) + } + + t.Logf("full simulation took %s", time.Since(testStart)) +} diff --git a/spn/access/token/scramble.go b/spn/access/token/scramble.go new file mode 100644 index 00000000..df96bcc6 --- /dev/null +++ b/spn/access/token/scramble.go @@ -0,0 +1,240 @@ +package token + +import ( + "fmt" + "sync" + + "github.com/mr-tron/base58" + + "github.com/safing/jess/lhash" + "github.com/safing/portbase/formats/dsd" +) + +const ( + scrambleSecretSize = 32 +) + +// ScrambleToken is token based on hashing. +type ScrambleToken struct { + Token []byte +} + +// Pack packs the token. +func (pbt *ScrambleToken) Pack() ([]byte, error) { + return pbt.Token, nil +} + +// UnpackScrambleToken unpacks the token. +func UnpackScrambleToken(token []byte) (*ScrambleToken, error) { + return &ScrambleToken{Token: token}, nil +} + +// ScrambleHandler is a handler for the scramble tokens. +type ScrambleHandler struct { + sync.Mutex + opts *ScrambleOptions + + storageLock sync.Mutex + Storage []*ScrambleToken + + verifiersLock sync.RWMutex + verifiers map[string]*ScrambleToken +} + +// ScrambleOptions are options for the ScrambleHandler. +type ScrambleOptions struct { + Zone string + Algorithm lhash.Algorithm + InitialTokens []string + InitialVerifiers []string + Fallback bool +} + +// ScrambleTokenRequest is a token request. +type ScrambleTokenRequest struct{} + +// IssuedScrambleTokens are issued scrambled tokens. +type IssuedScrambleTokens struct { + Tokens []*ScrambleToken +} + +// NewScrambleHandler creates a new scramble handler. +func NewScrambleHandler(opts ScrambleOptions) (*ScrambleHandler, error) { + sh := &ScrambleHandler{ + opts: &opts, + verifiers: make(map[string]*ScrambleToken, len(opts.InitialTokens)+len(opts.InitialVerifiers)), + } + + // Add initial tokens. + sh.Storage = make([]*ScrambleToken, len(opts.InitialTokens)) + for i, token := range opts.InitialTokens { + // Add to storage. + tokenData, err := base58.Decode(token) + if err != nil { + return nil, fmt.Errorf("failed to decode initial token %q: %w", token, err) + } + sh.Storage[i] = &ScrambleToken{ + Token: tokenData, + } + + // Add to verifiers. + scrambledToken := lhash.Digest(sh.opts.Algorithm, tokenData).Bytes() + sh.verifiers[string(scrambledToken)] = sh.Storage[i] + } + + // Add initial verifiers. + for _, verifier := range opts.InitialVerifiers { + verifierData, err := base58.Decode(verifier) + if err != nil { + return nil, fmt.Errorf("failed to decode verifier %q: %w", verifier, err) + } + sh.verifiers[string(verifierData)] = &ScrambleToken{} + } + + return sh, nil +} + +// Zone returns the zone name. +func (sh *ScrambleHandler) Zone() string { + return sh.opts.Zone +} + +// ShouldRequest returns whether the new tokens should be requested. +func (sh *ScrambleHandler) ShouldRequest() bool { + sh.storageLock.Lock() + defer sh.storageLock.Unlock() + + return len(sh.Storage) == 0 +} + +// Amount returns the current amount of tokens in this handler. +func (sh *ScrambleHandler) Amount() int { + sh.storageLock.Lock() + defer sh.storageLock.Unlock() + + return len(sh.Storage) +} + +// IsFallback returns whether this handler should only be used as a fallback. +func (sh *ScrambleHandler) IsFallback() bool { + return sh.opts.Fallback +} + +// CreateTokenRequest creates a token request to be sent to the token server. +func (sh *ScrambleHandler) CreateTokenRequest() (request *ScrambleTokenRequest) { + return &ScrambleTokenRequest{} +} + +// IssueTokens sign the requested tokens. +func (sh *ScrambleHandler) IssueTokens(request *ScrambleTokenRequest) (response *IssuedScrambleTokens, err error) { + // Copy the storage. + tokens := make([]*ScrambleToken, len(sh.Storage)) + copy(tokens, sh.Storage) + + return &IssuedScrambleTokens{ + Tokens: tokens, + }, nil +} + +// ProcessIssuedTokens processes the issued token from the server. +func (sh *ScrambleHandler) ProcessIssuedTokens(issuedTokens *IssuedScrambleTokens) error { + sh.verifiersLock.RLock() + defer sh.verifiersLock.RUnlock() + + // Validate tokens. + for i, newToken := range issuedTokens.Tokens { + // Scramle token. + scrambledToken := lhash.Digest(sh.opts.Algorithm, newToken.Token).Bytes() + + // Check if token is valid. + _, ok := sh.verifiers[string(scrambledToken)] + if !ok { + return fmt.Errorf("invalid token on #%d", i) + } + } + + // Copy to storage. + sh.Storage = issuedTokens.Tokens + + return nil +} + +// Verify verifies the given token. +func (sh *ScrambleHandler) Verify(token *Token) error { + if token.Zone != sh.opts.Zone { + return ErrZoneMismatch + } + + // Hash the data. + scrambledToken := lhash.Digest(sh.opts.Algorithm, token.Data).Bytes() + + sh.verifiersLock.RLock() + defer sh.verifiersLock.RUnlock() + + // Check if token is valid. + _, ok := sh.verifiers[string(scrambledToken)] + if !ok { + return ErrTokenInvalid + } + + return nil +} + +// GetToken returns a token. +func (sh *ScrambleHandler) GetToken() (*Token, error) { + sh.storageLock.Lock() + defer sh.storageLock.Unlock() + + if len(sh.Storage) == 0 { + return nil, ErrEmpty + } + + return &Token{ + Zone: sh.opts.Zone, + Data: sh.Storage[0].Token, + }, nil +} + +// ScrambleStorage is a storage for scramble tokens. +type ScrambleStorage struct { + Storage []*ScrambleToken +} + +// Save serializes and returns the current tokens. +func (sh *ScrambleHandler) Save() ([]byte, error) { + sh.storageLock.Lock() + defer sh.storageLock.Unlock() + + if len(sh.Storage) == 0 { + return nil, ErrEmpty + } + + s := &ScrambleStorage{ + Storage: sh.Storage, + } + + return dsd.Dump(s, dsd.CBOR) +} + +// Load loads the given tokens into the handler. +func (sh *ScrambleHandler) Load(data []byte) error { + sh.storageLock.Lock() + defer sh.storageLock.Unlock() + + s := &ScrambleStorage{} + _, err := dsd.Load(data, s) + if err != nil { + return err + } + + sh.Storage = s.Storage + return nil +} + +// Clear clears all the tokens in the handler. +func (sh *ScrambleHandler) Clear() { + sh.storageLock.Lock() + defer sh.storageLock.Unlock() + + sh.Storage = nil +} diff --git a/spn/access/token/scramble_gen_test.go b/spn/access/token/scramble_gen_test.go new file mode 100644 index 00000000..91a7d32e --- /dev/null +++ b/spn/access/token/scramble_gen_test.go @@ -0,0 +1,48 @@ +package token + +import ( + "crypto/rand" + "fmt" + "testing" + + "github.com/mr-tron/base58" + + "github.com/safing/jess/lhash" +) + +type genAlgs struct { + alg lhash.Algorithm + name string +} + +func TestGenerateScrambleKeys(t *testing.T) { + t.Parallel() + + for _, alg := range []genAlgs{ + {alg: lhash.SHA2_256, name: "SHA2_256"}, + {alg: lhash.SHA3_256, name: "SHA3_256"}, + {alg: lhash.SHA3_512, name: "SHA3_512"}, + {alg: lhash.BLAKE2b_256, name: "BLAKE2b_256"}, + } { + token := make([]byte, scrambleSecretSize) + n, err := rand.Read(token) + if err != nil { + t.Fatal(err) + } + if n != scrambleSecretSize { + t.Fatalf("only got %d bytes", n) + } + scrambledToken := lhash.Digest(alg.alg, token).Bytes() + + fmt.Printf( + "%s secret token: %s\n", + alg.name, + base58.Encode(token), + ) + fmt.Printf( + "%s scrambled (public) token: %s\n", + alg.name, + base58.Encode(scrambledToken), + ) + } +} diff --git a/spn/access/token/scramble_test.go b/spn/access/token/scramble_test.go new file mode 100644 index 00000000..765d7007 --- /dev/null +++ b/spn/access/token/scramble_test.go @@ -0,0 +1,84 @@ +package token + +import ( + "testing" + + "github.com/safing/jess/lhash" +) + +const ScrambleTestZone = "test-scramble" + +func init() { + // Combined testing config. + + h, err := NewScrambleHandler(ScrambleOptions{ + Zone: ScrambleTestZone, + Algorithm: lhash.SHA2_256, + InitialTokens: []string{"2VqJ8BvDew1tUpytZhR7tuvq7ToPpW3tQtHvu3veE3iW"}, + }) + if err != nil { + panic(err) + } + + err = RegisterScrambleHandler(h) + if err != nil { + panic(err) + } +} + +func TestScramble(t *testing.T) { + t.Parallel() + + opts := &ScrambleOptions{ + Zone: ScrambleTestZone, + Algorithm: lhash.SHA2_256, + } + + // Issuer + opts.InitialTokens = []string{"2VqJ8BvDew1tUpytZhR7tuvq7ToPpW3tQtHvu3veE3iW"} + issuer, err := NewScrambleHandler(*opts) + if err != nil { + t.Fatal(err) + } + + // Client + opts.InitialTokens = nil + opts.InitialVerifiers = []string{"Cy9tz37Xq9NiXGDRU9yicjGU62GjXskE9KqUmuoddSxaE3"} + client, err := NewScrambleHandler(*opts) + if err != nil { + t.Fatal(err) + } + + // Verifier + verifier, err := NewScrambleHandler(*opts) + if err != nil { + t.Fatal(err) + } + + // Play through the whole use case. + + request := client.CreateTokenRequest() + if err != nil { + t.Fatal(err) + } + + issuedTokens, err := issuer.IssueTokens(request) + if err != nil { + t.Fatal(err) + } + + err = client.ProcessIssuedTokens(issuedTokens) + if err != nil { + t.Fatal(err) + } + + token, err := client.GetToken() + if err != nil { + t.Fatal(err) + } + + err = verifier.Verify(token) + if err != nil { + t.Fatal(err) + } +} diff --git a/spn/access/token/token.go b/spn/access/token/token.go new file mode 100644 index 00000000..b93ed194 --- /dev/null +++ b/spn/access/token/token.go @@ -0,0 +1,83 @@ +package token + +import ( + "bytes" + "errors" + "fmt" + "strings" + + "github.com/mr-tron/base58" + + "github.com/safing/portbase/container" +) + +// Token represents a token, consisting of a zone (name) and some data. +type Token struct { + Zone string + Data []byte +} + +// GetToken returns a token of the given zone. +func GetToken(zone string) (*Token, error) { + handler, ok := GetHandler(zone) + if !ok { + return nil, ErrZoneUnknown + } + + return handler.GetToken() +} + +// VerifyToken verifies the given token. +func VerifyToken(token *Token) error { + handler, ok := GetHandler(token.Zone) + if !ok { + return ErrZoneUnknown + } + + return handler.Verify(token) +} + +// Raw returns the raw format of the token. +func (c *Token) Raw() []byte { + cont := container.New() + cont.Append([]byte(c.Zone)) + cont.Append([]byte(":")) + cont.Append(c.Data) + return cont.CompileData() +} + +// String returns the stringified format of the token. +func (c *Token) String() string { + return c.Zone + ":" + base58.Encode(c.Data) +} + +// ParseRawToken parses a raw token. +func ParseRawToken(code []byte) (*Token, error) { + splitted := bytes.SplitN(code, []byte(":"), 2) + if len(splitted) < 2 { + return nil, errors.New("invalid code format: zone/data separator missing") + } + + return &Token{ + Zone: string(splitted[0]), + Data: splitted[1], + }, nil +} + +// ParseToken parses a stringified token. +func ParseToken(code string) (*Token, error) { + splitted := strings.SplitN(code, ":", 2) + if len(splitted) < 2 { + return nil, errors.New("invalid code format: zone/data separator missing") + } + + data, err := base58.Decode(splitted[1]) + if err != nil { + return nil, fmt.Errorf("invalid code format: %w", err) + } + + return &Token{ + Zone: splitted[0], + Data: data, + }, nil +} diff --git a/spn/access/token/token_test.go b/spn/access/token/token_test.go new file mode 100644 index 00000000..b132265a --- /dev/null +++ b/spn/access/token/token_test.go @@ -0,0 +1,33 @@ +package token + +import ( + "testing" + + "github.com/safing/portbase/rng" +) + +func TestToken(t *testing.T) { + t.Parallel() + + randomData, err := rng.Bytes(32) + if err != nil { + t.Fatal(err) + } + + c := &Token{ + Zone: "test", + Data: randomData, + } + + s := c.String() + _, err = ParseToken(s) + if err != nil { + t.Fatal(err) + } + + r := c.Raw() + _, err = ParseRawToken(r) + if err != nil { + t.Fatal(err) + } +} diff --git a/spn/access/zones.go b/spn/access/zones.go new file mode 100644 index 00000000..1f9c954b --- /dev/null +++ b/spn/access/zones.go @@ -0,0 +1,257 @@ +package access + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/tevino/abool" + + "github.com/safing/jess/lhash" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/access/token" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/terminal" +) + +var ( + // ExpandAndConnectZones are the zones that grant access to the expand and + // connect operations. + ExpandAndConnectZones = []string{"pblind1", "alpha2", "fallback1"} + + zonePermissions = map[string]terminal.Permission{ + "pblind1": terminal.AddPermissions(terminal.MayExpand, terminal.MayConnect), + "alpha2": terminal.AddPermissions(terminal.MayExpand, terminal.MayConnect), + "fallback1": terminal.AddPermissions(terminal.MayExpand, terminal.MayConnect), + } + persistentZones = ExpandAndConnectZones + + enableTestMode = abool.New() +) + +// EnableTestMode enables the test mode, leading the access module to only +// register a test zone. +// This should not be used to test the access module itself. +func EnableTestMode() { + enableTestMode.Set() +} + +// InitializeZones initialized the permission zones. +// It initializes the test zones, if EnableTestMode was called before. +// Must only be called once. +func InitializeZones() error { + // Check if we are testing. + if enableTestMode.IsSet() { + return initializeTestZone() + } + + // Special client zone config. + var requestSignalHandler func(token.Handler) + if conf.Client() { + requestSignalHandler = shouldRequestTokensHandler + } + + // Register pblind1 as the first primary zone. + ph, err := token.NewPBlindHandler(token.PBlindOptions{ + Zone: "pblind1", + CurveName: "P-256", + PublicKey: "eXoJXzXbM66UEsM2eVi9HwyBPLMfVnNrC7gNrsfMUJDs", + UseSerials: true, + BatchSize: 1000, + RandomizeOrder: true, + SignalShouldRequest: requestSignalHandler, + }) + if err != nil { + return fmt.Errorf("failed to create pblind1 token handler: %w", err) + } + err = token.RegisterPBlindHandler(ph) + if err != nil { + return fmt.Errorf("failed to register pblind1 token handler: %w", err) + } + + // Register fallback1 zone as fallback when the issuer is not available. + sh, err := token.NewScrambleHandler(token.ScrambleOptions{ + Zone: "fallback1", + Algorithm: lhash.BLAKE2b_256, + InitialVerifiers: []string{"ZwkQoaAttVBMURzeLzNXokFBMAMUUwECfM1iHojcVKBmjk"}, + Fallback: true, + }) + if err != nil { + return fmt.Errorf("failed to create fallback1 token handler: %w", err) + } + err = token.RegisterScrambleHandler(sh) + if err != nil { + return fmt.Errorf("failed to register fallback1 token handler: %w", err) + } + + // Register alpha2 zone for transition phase. + sh, err = token.NewScrambleHandler(token.ScrambleOptions{ + Zone: "alpha2", + Algorithm: lhash.BLAKE2b_256, + InitialVerifiers: []string{"ZwojEvXZmAv7SZdNe7m94Xzu7F9J8vULqKf7QYtoTpN2tH"}, + }) + if err != nil { + return fmt.Errorf("failed to create alpha2 token handler: %w", err) + } + err = token.RegisterScrambleHandler(sh) + if err != nil { + return fmt.Errorf("failed to register alpha2 token handler: %w", err) + } + + return nil +} + +func initializeTestZone() error { + // Safeguard checks if we should really enable the test zone. + if !strings.HasSuffix(os.Args[0], ".test") { + return errors.New("tried to enable test mode, but no test binary was detected") + } + if token.RegistrySize() > 0 { + return fmt.Errorf("tried to enable test zone, but %d handlers are already registered", token.RegistrySize()) + } + + // Reset zones. + token.ResetRegistry() + + // Set eligible zones. + ExpandAndConnectZones = []string{"unittest"} + zonePermissions = map[string]terminal.Permission{ + "unittest": terminal.AddPermissions(terminal.MayExpand, terminal.MayConnect), + } + + // Register unittest zone as for testing. + sh, err := token.NewScrambleHandler(token.ScrambleOptions{ + Zone: "unittest", + Algorithm: lhash.BLAKE2b_256, + InitialTokens: []string{"6jFqLA93uSLL52utGKrvctG3ZfopSQ8WFqjsRK1c2Svt"}, + InitialVerifiers: []string{"ZwoEoL59sr81s7WnF2vydGzjeejE3u8CqVafig1NTQzUr7"}, + }) + if err != nil { + return fmt.Errorf("failed to create unittest token handler: %w", err) + } + err = token.RegisterScrambleHandler(sh) + if err != nil { + return fmt.Errorf("failed to register unittest token handler: %w", err) + } + + return nil +} + +func shouldRequestTokensHandler(_ token.Handler) { + // accountUpdateTask is always set in client mode and when the module is online. + // Check if it's set in case this gets executed in other circumstances. + if accountUpdateTask == nil { + log.Warningf("spn/access: trying to trigger account update, but the task is not available") + return + } + + accountUpdateTask.StartASAP() +} + +// GetTokenAmount returns the amount of tokens for the given zones. +func GetTokenAmount(zones []string) (regular, fallback int) { +handlerLoop: + for _, zone := range zones { + // Get handler and check if it should be used. + handler, ok := token.GetHandler(zone) + if !ok { + log.Warningf("spn/access: use of non-registered zone %q", zone) + continue handlerLoop + } + + if handler.IsFallback() { + fallback += handler.Amount() + } else { + regular += handler.Amount() + } + } + + return +} + +// ShouldRequest returns whether tokens should be requested for the given zones. +func ShouldRequest(zones []string) (shouldRequest bool) { +handlerLoop: + for _, zone := range zones { + // Get handler and check if it should be used. + handler, ok := token.GetHandler(zone) + if !ok { + log.Warningf("spn/access: use of non-registered zone %q", zone) + continue handlerLoop + } + + // Go through all handlers every time as this will be the case anyway most + // of the time and will help us better catch zone misconfiguration. + if handler.ShouldRequest() { + shouldRequest = true + } + } + + return shouldRequest +} + +// GetToken returns a token of one of the given zones. +func GetToken(zones []string) (t *token.Token, err error) { +handlerSelection: + for _, zone := range zones { + // Get handler and check if it should be used. + handler, ok := token.GetHandler(zone) + switch { + case !ok: + log.Warningf("spn/access: use of non-registered zone %q", zone) + continue handlerSelection + case handler.IsFallback() && !TokenIssuerIsFailing(): + // Skip fallback zone if everything works. + continue handlerSelection + } + + // Get token from handler. + t, err = token.GetToken(zone) + if err == nil { + return t, nil + } + } + + // Return existing error, if exists. + if err != nil { + return nil, err + } + return nil, token.ErrEmpty +} + +// VerifyRawToken verifies a raw token. +func VerifyRawToken(data []byte) (granted terminal.Permission, err error) { + t, err := token.ParseRawToken(data) + if err != nil { + return 0, fmt.Errorf("failed to parse token: %w", err) + } + + return VerifyToken(t) +} + +// VerifyToken verifies a token. +func VerifyToken(t *token.Token) (granted terminal.Permission, err error) { + handler, ok := token.GetHandler(t.Zone) + if !ok { + return terminal.NoPermission, token.ErrZoneUnknown + } + + // Check if the token is a fallback token. + if handler.IsFallback() && !healthCheck() { + return terminal.NoPermission, ErrFallbackNotAvailable + } + + // Verify token. + err = handler.Verify(t) + if err != nil { + return 0, fmt.Errorf("failed to verify token: %w", err) + } + + // Return permission of zone. + granted, ok = zonePermissions[t.Zone] + if !ok { + return terminal.NoPermission, nil + } + return granted, nil +} diff --git a/spn/cabin/config-public.go b/spn/cabin/config-public.go new file mode 100644 index 00000000..4ae733ae --- /dev/null +++ b/spn/cabin/config-public.go @@ -0,0 +1,392 @@ +package cabin + +import ( + "fmt" + "net" + "os" + + "github.com/safing/portbase/config" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/profile/endpoints" + "github.com/safing/portmaster/spn/hub" +) + +// Configuration Keys. +var ( + // Name of the node. + publicCfgOptionNameKey = "spn/publicHub/name" + publicCfgOptionName config.StringOption + publicCfgOptionNameDefault = "" + publicCfgOptionNameOrder = 512 + + // Person or organisation, who is in control of the node (should be same for all nodes of this person or organisation). + publicCfgOptionGroupKey = "spn/publicHub/group" + publicCfgOptionGroup config.StringOption + publicCfgOptionGroupDefault = "" + publicCfgOptionGroupOrder = 513 + + // Contact possibility (recommended, but optional). + publicCfgOptionContactAddressKey = "spn/publicHub/contactAddress" + publicCfgOptionContactAddress config.StringOption + publicCfgOptionContactAddressDefault = "" + publicCfgOptionContactAddressOrder = 514 + + // Type of service of the contact address, if not email. + publicCfgOptionContactServiceKey = "spn/publicHub/contactService" + publicCfgOptionContactService config.StringOption + publicCfgOptionContactServiceDefault = "" + publicCfgOptionContactServiceOrder = 515 + + // Hosters - supply chain (reseller, hosting provider, datacenter operator, ...). + publicCfgOptionHostersKey = "spn/publicHub/hosters" + publicCfgOptionHosters config.StringArrayOption + publicCfgOptionHostersDefault = []string{} + publicCfgOptionHostersOrder = 516 + + // Datacenter + // Format: CC-COMPANY-INTERNALCODE + // Eg: DE-Hetzner-FSN1-DC5 + //. + publicCfgOptionDatacenterKey = "spn/publicHub/datacenter" + publicCfgOptionDatacenter config.StringOption + publicCfgOptionDatacenterDefault = "" + publicCfgOptionDatacenterOrder = 517 + + // Network Location and Access. + + // IPv4 must be global and accessible. + publicCfgOptionIPv4Key = "spn/publicHub/ip4" + publicCfgOptionIPv4 config.StringOption + publicCfgOptionIPv4Default = "" + publicCfgOptionIPv4Order = 518 + + // IPv6 must be global and accessible. + publicCfgOptionIPv6Key = "spn/publicHub/ip6" + publicCfgOptionIPv6 config.StringOption + publicCfgOptionIPv6Default = "" + publicCfgOptionIPv6Order = 519 + + // Transports. + publicCfgOptionTransportsKey = "spn/publicHub/transports" + publicCfgOptionTransports config.StringArrayOption + publicCfgOptionTransportsDefault = []string{ + "tcp:17", + } + publicCfgOptionTransportsOrder = 520 + + // Entry Policy. + publicCfgOptionEntryKey = "spn/publicHub/entry" + publicCfgOptionEntry config.StringArrayOption + publicCfgOptionEntryDefault = []string{} + publicCfgOptionEntryOrder = 521 + + // Exit Policy. + publicCfgOptionExitKey = "spn/publicHub/exit" + publicCfgOptionExit config.StringArrayOption + publicCfgOptionExitDefault = []string{"- * TCP/25"} + publicCfgOptionExitOrder = 522 + + // Allow Unencrypted. + publicCfgOptionAllowUnencryptedKey = "spn/publicHub/allowUnencrypted" + publicCfgOptionAllowUnencrypted config.BoolOption + publicCfgOptionAllowUnencryptedDefault = false + publicCfgOptionAllowUnencryptedOrder = 523 +) + +func prepPublicHubConfig() error { + err := config.Register(&config.Option{ + Name: "Name", + Key: publicCfgOptionNameKey, + Description: "Human readable name of the Hub.", + OptType: config.OptTypeString, + ExpertiseLevel: config.ExpertiseLevelExpert, + RequiresRestart: true, + DefaultValue: publicCfgOptionNameDefault, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: publicCfgOptionNameOrder, + }, + }) + if err != nil { + return err + } + publicCfgOptionName = config.GetAsString(publicCfgOptionNameKey, publicCfgOptionNameDefault) + + err = config.Register(&config.Option{ + Name: "Group", + Key: publicCfgOptionGroupKey, + Description: "Name of the hub group this Hub belongs to.", + OptType: config.OptTypeString, + ExpertiseLevel: config.ExpertiseLevelExpert, + RequiresRestart: true, + DefaultValue: publicCfgOptionGroupDefault, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: publicCfgOptionGroupOrder, + }, + }) + if err != nil { + return err + } + publicCfgOptionGroup = config.GetAsString(publicCfgOptionGroupKey, publicCfgOptionGroupDefault) + + err = config.Register(&config.Option{ + Name: "Contact Address", + Key: publicCfgOptionContactAddressKey, + Description: "Contact address where the Hub operator can be reached.", + OptType: config.OptTypeString, + ExpertiseLevel: config.ExpertiseLevelExpert, + RequiresRestart: true, + DefaultValue: publicCfgOptionContactAddressDefault, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: publicCfgOptionContactAddressOrder, + }, + }) + if err != nil { + return err + } + publicCfgOptionContactAddress = config.GetAsString(publicCfgOptionContactAddressKey, publicCfgOptionContactAddressDefault) + + err = config.Register(&config.Option{ + Name: "Contact Service", + Key: publicCfgOptionContactServiceKey, + Description: "Name of the service the contact address corresponds to, if not email.", + OptType: config.OptTypeString, + ExpertiseLevel: config.ExpertiseLevelExpert, + RequiresRestart: true, + DefaultValue: publicCfgOptionContactServiceDefault, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: publicCfgOptionContactServiceOrder, + }, + }) + if err != nil { + return err + } + publicCfgOptionContactService = config.GetAsString(publicCfgOptionContactServiceKey, publicCfgOptionContactServiceDefault) + + err = config.Register(&config.Option{ + Name: "Hosters", + Key: publicCfgOptionHostersKey, + Description: "List of all involved entities and organisations that are involved in hosting this Hub.", + OptType: config.OptTypeStringArray, + ExpertiseLevel: config.ExpertiseLevelExpert, + RequiresRestart: true, + DefaultValue: publicCfgOptionHostersDefault, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: publicCfgOptionHostersOrder, + }, + }) + if err != nil { + return err + } + publicCfgOptionHosters = config.GetAsStringArray(publicCfgOptionHostersKey, publicCfgOptionHostersDefault) + + err = config.Register(&config.Option{ + Name: "Datacenter", + Key: publicCfgOptionDatacenterKey, + Description: "Identifier of the datacenter this Hub is hosted in.", + OptType: config.OptTypeString, + ExpertiseLevel: config.ExpertiseLevelExpert, + RequiresRestart: true, + DefaultValue: publicCfgOptionDatacenterDefault, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: publicCfgOptionDatacenterOrder, + }, + }) + if err != nil { + return err + } + publicCfgOptionDatacenter = config.GetAsString(publicCfgOptionDatacenterKey, publicCfgOptionDatacenterDefault) + + err = config.Register(&config.Option{ + Name: "IPv4", + Key: publicCfgOptionIPv4Key, + Description: "IPv4 address of this Hub. Must be globally reachable.", + OptType: config.OptTypeString, + ExpertiseLevel: config.ExpertiseLevelExpert, + RequiresRestart: true, + DefaultValue: publicCfgOptionIPv4Default, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: publicCfgOptionIPv4Order, + }, + }) + if err != nil { + return err + } + publicCfgOptionIPv4 = config.GetAsString(publicCfgOptionIPv4Key, publicCfgOptionIPv4Default) + + err = config.Register(&config.Option{ + Name: "IPv6", + Key: publicCfgOptionIPv6Key, + Description: "IPv6 address of this Hub. Must be globally reachable.", + OptType: config.OptTypeString, + ExpertiseLevel: config.ExpertiseLevelExpert, + RequiresRestart: true, + DefaultValue: publicCfgOptionIPv6Default, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: publicCfgOptionIPv6Order, + }, + }) + if err != nil { + return err + } + publicCfgOptionIPv6 = config.GetAsString(publicCfgOptionIPv6Key, publicCfgOptionIPv6Default) + + err = config.Register(&config.Option{ + Name: "Transports", + Key: publicCfgOptionTransportsKey, + Description: "List of transports this Hub supports.", + OptType: config.OptTypeStringArray, + ExpertiseLevel: config.ExpertiseLevelExpert, + RequiresRestart: true, + DefaultValue: publicCfgOptionTransportsDefault, + ValidationFunc: func(value any) error { + if transports, ok := value.([]string); ok { + for i, transport := range transports { + if _, err := hub.ParseTransport(transport); err != nil { + return fmt.Errorf("failed to parse transport #%d: %w", i, err) + } + } + } else { + return fmt.Errorf("not a []string, but %T", value) + } + return nil + }, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: publicCfgOptionTransportsOrder, + }, + }) + if err != nil { + return err + } + publicCfgOptionTransports = config.GetAsStringArray(publicCfgOptionTransportsKey, publicCfgOptionTransportsDefault) + + err = config.Register(&config.Option{ + Name: "Entry", + Key: publicCfgOptionEntryKey, + Description: "Define an entry policy. The format is the same for the endpoint lists. Default is permit.", + OptType: config.OptTypeStringArray, + ExpertiseLevel: config.ExpertiseLevelExpert, + RequiresRestart: true, + DefaultValue: publicCfgOptionEntryDefault, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: publicCfgOptionEntryOrder, + config.DisplayHintAnnotation: endpoints.DisplayHintEndpointList, + }, + }) + if err != nil { + return err + } + publicCfgOptionEntry = config.GetAsStringArray(publicCfgOptionEntryKey, publicCfgOptionEntryDefault) + + err = config.Register(&config.Option{ + Name: "Exit", + Key: publicCfgOptionExitKey, + Description: "Define an exit policy. The format is the same for the endpoint lists. Default is permit.", + OptType: config.OptTypeStringArray, + ExpertiseLevel: config.ExpertiseLevelExpert, + RequiresRestart: true, + DefaultValue: publicCfgOptionExitDefault, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: publicCfgOptionExitOrder, + config.DisplayHintAnnotation: endpoints.DisplayHintEndpointList, + }, + }) + if err != nil { + return err + } + publicCfgOptionExit = config.GetAsStringArray(publicCfgOptionExitKey, publicCfgOptionExitDefault) + + err = config.Register(&config.Option{ + Name: "Allow Unencrypted Connections", + Key: publicCfgOptionAllowUnencryptedKey, + Description: "Advertise that this Hub is available for handling unencrypted connections, as detected by clients.", + OptType: config.OptTypeBool, + ExpertiseLevel: config.ExpertiseLevelExpert, + RequiresRestart: true, + DefaultValue: publicCfgOptionAllowUnencryptedDefault, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: publicCfgOptionAllowUnencryptedOrder, + }, + }) + if err != nil { + return err + } + publicCfgOptionAllowUnencrypted = config.GetAsBool(publicCfgOptionAllowUnencryptedKey, publicCfgOptionAllowUnencryptedDefault) + + // update defaults from system + setDynamicPublicDefaults() + + return nil +} + +func getPublicHubInfo() *hub.Announcement { + // get configuration + info := &hub.Announcement{ + Name: publicCfgOptionName(), + Group: publicCfgOptionGroup(), + ContactAddress: publicCfgOptionContactAddress(), + ContactService: publicCfgOptionContactService(), + Hosters: publicCfgOptionHosters(), + Datacenter: publicCfgOptionDatacenter(), + Transports: publicCfgOptionTransports(), + Entry: publicCfgOptionEntry(), + Exit: publicCfgOptionExit(), + Flags: []string{}, + } + + if publicCfgOptionAllowUnencrypted() { + info.Flags = append(info.Flags, hub.FlagAllowUnencrypted) + } + + ip4 := publicCfgOptionIPv4() + if ip4 != "" { + ip := net.ParseIP(ip4) + if ip == nil { + log.Warningf("spn/cabin: invalid %s config: %s", publicCfgOptionIPv4Key, ip4) + } else { + info.IPv4 = ip + } + } + + ip6 := publicCfgOptionIPv6() + if ip6 != "" { + ip := net.ParseIP(ip6) + if ip == nil { + log.Warningf("spn/cabin: invalid %s config: %s", publicCfgOptionIPv6Key, ip6) + } else { + info.IPv6 = ip + } + } + + return info +} + +func setDynamicPublicDefaults() { + // name + hostname, err := os.Hostname() + if err == nil { + err := config.SetDefaultConfigOption(publicCfgOptionNameKey, hostname) + if err != nil { + log.Warningf("spn/cabin: failed to set %s default to %s", publicCfgOptionNameKey, hostname) + } + } + + // IPs + v4IPs, v6IPs, err := netenv.GetAssignedGlobalAddresses() + if err != nil { + log.Warningf("spn/cabin: failed to get assigned addresses: %s", err) + return + } + if len(v4IPs) == 1 { + err = config.SetDefaultConfigOption(publicCfgOptionIPv4Key, v4IPs[0].String()) + if err != nil { + log.Warningf("spn/cabin: failed to set %s default to %s", publicCfgOptionIPv4Key, v4IPs[0].String()) + } + } + if len(v6IPs) == 1 { + err = config.SetDefaultConfigOption(publicCfgOptionIPv6Key, v6IPs[0].String()) + if err != nil { + log.Warningf("spn/cabin: failed to set %s default to %s", publicCfgOptionIPv6Key, v6IPs[0].String()) + } + } +} diff --git a/spn/cabin/database.go b/spn/cabin/database.go new file mode 100644 index 00000000..41097530 --- /dev/null +++ b/spn/cabin/database.go @@ -0,0 +1,98 @@ +package cabin + +import ( + "errors" + "fmt" + + "github.com/safing/portbase/database" + "github.com/safing/portbase/database/record" + "github.com/safing/portmaster/spn/hub" +) + +var db = database.NewInterface(nil) + +// LoadIdentity loads an identify with the given key. +func LoadIdentity(key string) (id *Identity, changed bool, err error) { + r, err := db.Get(key) + if err != nil { + return nil, false, err + } + id, err = EnsureIdentity(r) + if err != nil { + return nil, false, fmt.Errorf("failed to parse identity: %w", err) + } + + // Check if required fields are present. + switch { + case id.Hub == nil: + return nil, false, errors.New("missing id.Hub") + case id.Signet == nil: + return nil, false, errors.New("missing id.Signet") + case id.Hub.Info == nil: + return nil, false, errors.New("missing hub.Info") + case id.Hub.Status == nil: + return nil, false, errors.New("missing hub.Status") + case id.ID != id.Hub.ID: + return nil, false, errors.New("hub.ID mismatch") + case id.ID != id.Hub.Info.ID: + return nil, false, errors.New("hub.Info.ID mismatch") + case id.Map == "": + return nil, false, errors.New("invalid id.Map") + case id.Hub.Map == "": + return nil, false, errors.New("invalid hub.Map") + case id.Hub.FirstSeen.IsZero(): + return nil, false, errors.New("missing hub.FirstSeen") + case id.Hub.Info.Timestamp == 0: + return nil, false, errors.New("missing hub.Info.Timestamp") + case id.Hub.Status.Timestamp == 0: + return nil, false, errors.New("missing hub.Status.Timestamp") + } + + // Run a initial maintenance routine. + infoChanged, err := id.MaintainAnnouncement(nil, true) + if err != nil { + return nil, false, fmt.Errorf("failed to initialize announcement: %w", err) + } + statusChanged, err := id.MaintainStatus(nil, nil, nil, true) + if err != nil { + return nil, false, fmt.Errorf("failed to initialize status: %w", err) + } + + // Ensure the Measurements reset the values. + measurements := id.Hub.GetMeasurements() + measurements.SetLatency(0) + measurements.SetCapacity(0) + measurements.SetCalculatedCost(hub.MaxCalculatedCost) + + return id, infoChanged || statusChanged, nil +} + +// EnsureIdentity makes sure a database record is an Identity. +func EnsureIdentity(r record.Record) (*Identity, error) { + // unwrap + if r.IsWrapped() { + // only allocate a new struct, if we need it + id := &Identity{} + err := record.Unwrap(r, id) + if err != nil { + return nil, err + } + return id, nil + } + + // or adjust type + id, ok := r.(*Identity) + if !ok { + return nil, fmt.Errorf("record not of type *Identity, but %T", r) + } + return id, nil +} + +// Save saves the Identity to the database. +func (id *Identity) Save() error { + if !id.KeyIsSet() { + return errors.New("no key set") + } + + return db.Put(id) +} diff --git a/spn/cabin/identity.go b/spn/cabin/identity.go new file mode 100644 index 00000000..0be583cf --- /dev/null +++ b/spn/cabin/identity.go @@ -0,0 +1,311 @@ +package cabin + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/safing/jess" + "github.com/safing/jess/tools" + "github.com/safing/portbase/database/record" + "github.com/safing/portbase/info" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/hub" +) + +const ( + // DefaultIDKeyScheme is the default jess tool for creating ID keys. + DefaultIDKeyScheme = "Ed25519" + + // DefaultIDKeySecurityLevel is the default security level for creating ID keys. + DefaultIDKeySecurityLevel = 256 // Ed25519 security level is fixed, setting is ignored. +) + +// Identity holds the identity of a Hub. +type Identity struct { + record.Base + + ID string + Map string + Hub *hub.Hub + Signet *jess.Signet + + ExchKeys map[string]*ExchKey + + infoExportCache []byte + statusExportCache []byte +} + +// Lock locks the Identity through the Hub lock. +func (id *Identity) Lock() { + id.Hub.Lock() +} + +// Unlock unlocks the Identity through the Hub lock. +func (id *Identity) Unlock() { + id.Hub.Unlock() +} + +// ExchKey holds the private information of a HubKey. +type ExchKey struct { + Created time.Time + Expires time.Time + key *jess.Signet + tool *tools.Tool +} + +// CreateIdentity creates a new identity. +func CreateIdentity(ctx context.Context, mapName string) (*Identity, error) { + id := &Identity{ + Map: mapName, + ExchKeys: make(map[string]*ExchKey), + } + + // create signet + signet, recipient, err := hub.CreateHubSignet(DefaultIDKeyScheme, DefaultIDKeySecurityLevel) + if err != nil { + return nil, err + } + id.Signet = signet + id.ID = signet.ID + id.Hub = &hub.Hub{ + ID: id.ID, + Map: mapName, + PublicKey: recipient, + } + + // initial maintenance routine + _, err = id.MaintainAnnouncement(nil, true) + if err != nil { + return nil, fmt.Errorf("failed to initialize announcement: %w", err) + } + _, err = id.MaintainStatus([]*hub.Lane{}, new(int), nil, true) + if err != nil { + return nil, fmt.Errorf("failed to initialize status: %w", err) + } + + return id, nil +} + +// MaintainAnnouncement maintains the Hub's Announcenemt and returns whether +// there was a change that should be communicated to other Hubs. +// If newInfo is nil, it will be derived from configuration. +func (id *Identity) MaintainAnnouncement(newInfo *hub.Announcement, selfcheck bool) (changed bool, err error) { + id.Lock() + defer id.Unlock() + + // Populate new info with data. + if newInfo == nil { + newInfo = getPublicHubInfo() + } + newInfo.ID = id.Hub.ID + if id.Hub.Info != nil { + newInfo.Timestamp = id.Hub.Info.Timestamp + } + if !newInfo.Equal(id.Hub.Info) { + changed = true + } + + if changed { + // Update timestamp. + newInfo.Timestamp = time.Now().Unix() + } + + if changed || selfcheck { + // Export new data. + newInfoData, err := newInfo.Export(id.signingEnvelope()) + if err != nil { + return false, fmt.Errorf("failed to export: %w", err) + } + + // Apply the status as all other Hubs would in order to check if it's valid. + _, _, _, err = hub.ApplyAnnouncement(id.Hub, newInfoData, conf.MainMapName, conf.MainMapScope, true) + if err != nil { + return false, fmt.Errorf("failed to apply new announcement: %w", err) + } + id.infoExportCache = newInfoData + + // Save message to hub message storage. + err = hub.SaveHubMsg(id.ID, conf.MainMapName, hub.MsgTypeAnnouncement, newInfoData) + if err != nil { + log.Warningf("spn/cabin: failed to save own new/updated announcement of %s: %s", id.ID, err) + } + } + + return changed, nil +} + +// MaintainStatus maintains the Hub's Status and returns whether there was a change that should be communicated to other Hubs. +func (id *Identity) MaintainStatus(lanes []*hub.Lane, load *int, flags []string, selfcheck bool) (changed bool, err error) { + id.Lock() + defer id.Unlock() + + // Create a new status or make a copy of the status for editing. + var newStatus *hub.Status + if id.Hub.Status != nil { + newStatus = id.Hub.Status.Copy() + } else { + newStatus = &hub.Status{} + } + + // Update software version. + if newStatus.Version != info.Version() { + newStatus.Version = info.Version() + changed = true + } + + // Update keys. + keysChanged, err := id.MaintainExchKeys(newStatus, time.Now()) + if err != nil { + return false, fmt.Errorf("failed to maintain keys: %w", err) + } + if keysChanged { + changed = true + } + + // Update lanes. + if lanes != nil && !hub.LanesEqual(newStatus.Lanes, lanes) { + newStatus.Lanes = lanes + changed = true + } + + // Update load. + if load != nil && newStatus.Load != *load { + newStatus.Load = *load + changed = true + } + + // Update flags. + if !hub.FlagsEqual(newStatus.Flags, flags) { + newStatus.Flags = flags + changed = true + } + + // Update timestamp if something changed. + if changed { + newStatus.Timestamp = time.Now().Unix() + } + + if changed || selfcheck { + // Export new data. + newStatusData, err := newStatus.Export(id.signingEnvelope()) + if err != nil { + return false, fmt.Errorf("failed to export: %w", err) + } + + // Apply the status as all other Hubs would in order to check if it's valid. + _, _, _, err = hub.ApplyStatus(id.Hub, newStatusData, conf.MainMapName, conf.MainMapScope, true) + if err != nil { + return false, fmt.Errorf("failed to apply new status: %w", err) + } + id.statusExportCache = newStatusData + + // Save message to hub message storage. + err = hub.SaveHubMsg(id.ID, conf.MainMapName, hub.MsgTypeStatus, newStatusData) + if err != nil { + log.Warningf("spn/cabin: failed to save own new/updated status: %s", err) + } + } + + return changed, nil +} + +// MakeOfflineStatus creates and signs an offline status message. +func (id *Identity) MakeOfflineStatus() (offlineStatusExport []byte, err error) { + // Make offline status. + newStatus := &hub.Status{ + Timestamp: time.Now().Unix(), + Version: info.Version(), + Flags: []string{hub.FlagOffline}, + } + + // Export new data. + newStatusData, err := newStatus.Export(id.signingEnvelope()) + if err != nil { + return nil, fmt.Errorf("failed to export: %w", err) + } + + return newStatusData, nil +} + +func (id *Identity) signingEnvelope() *jess.Envelope { + env := jess.NewUnconfiguredEnvelope() + env.SuiteID = jess.SuiteSignV1 + env.Senders = []*jess.Signet{id.Signet} + + return env +} + +// ExportAnnouncement serializes and signs the Announcement. +func (id *Identity) ExportAnnouncement() ([]byte, error) { + id.Lock() + defer id.Unlock() + + if id.infoExportCache == nil { + return nil, errors.New("announcement not exported") + } + + return id.infoExportCache, nil +} + +// ExportStatus serializes and signs the Status. +func (id *Identity) ExportStatus() ([]byte, error) { + id.Lock() + defer id.Unlock() + + if id.statusExportCache == nil { + return nil, errors.New("status not exported") + } + + return id.statusExportCache, nil +} + +// SignHubMsg signs a data blob with the identity's private key. +func (id *Identity) SignHubMsg(data []byte) ([]byte, error) { + return hub.SignHubMsg(data, id.signingEnvelope(), false) +} + +// GetSignet returns the private exchange key with the given ID. +func (id *Identity) GetSignet(keyID string, recipient bool) (*jess.Signet, error) { + if recipient { + return nil, errors.New("cabin.Identity only serves private keys") + } + + id.Lock() + defer id.Unlock() + + key, ok := id.ExchKeys[keyID] + if !ok { + return nil, errors.New("the requested key does not exist") + } + if time.Now().After(key.Expires) || key.key == nil { + return nil, errors.New("the requested key has expired") + } + + return key.key, nil +} + +func (ek *ExchKey) toHubKey() (*hub.Key, error) { + if ek.key == nil { + return nil, errors.New("no key") + } + + // export public key + rcpt, err := ek.key.AsRecipient() + if err != nil { + return nil, err + } + err = rcpt.StoreKey() + if err != nil { + return nil, err + } + + // repackage + return &hub.Key{ + Scheme: rcpt.Scheme, + Key: rcpt.Key, + Expires: ek.Expires.Unix(), + }, nil +} diff --git a/spn/cabin/identity_test.go b/spn/cabin/identity_test.go new file mode 100644 index 00000000..6ad0530d --- /dev/null +++ b/spn/cabin/identity_test.go @@ -0,0 +1,129 @@ +package cabin + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/hub" +) + +func TestIdentity(t *testing.T) { + t.Parallel() + + // Register config options for public hub. + if err := prepPublicHubConfig(); err != nil { + t.Fatal(err) + } + + // Create new identity. + identityTestKey := "core:spn/public/identity" + id, err := CreateIdentity(module.Ctx, conf.MainMapName) + if err != nil { + t.Fatal(err) + } + id.SetKey(identityTestKey) + + // Check values + // Identity + assert.NotEmpty(t, id.ID, "id.ID must be set") + assert.NotEmpty(t, id.Map, "id.Map must be set") + assert.NotNil(t, id.Signet, "id.Signet must be set") + assert.NotNil(t, id.infoExportCache, "id.infoExportCache must be set") + assert.NotNil(t, id.statusExportCache, "id.statusExportCache must be set") + // Hub + assert.NotEmpty(t, id.Hub.ID, "hub.ID must be set") + assert.NotEmpty(t, id.Hub.Map, "hub.Map must be set") + assert.NotZero(t, id.Hub.FirstSeen, "hub.FirstSeen must be set") + // Info + assert.NotEmpty(t, id.Hub.Info.ID, "info.ID must be set") + assert.NotEqual(t, 0, id.Hub.Info.Timestamp, "info.Timestamp must be set") + assert.NotEqual(t, "", id.Hub.Info.Name, "info.Name must be set (to hostname)") + // Status + assert.NotEqual(t, 0, id.Hub.Status.Timestamp, "status.Timestamp must be set") + assert.NotEmpty(t, id.Hub.Status.Keys, "status.Keys must be set") + + fmt.Printf("id: %+v\n", id) + fmt.Printf("id.hub: %+v\n", id.Hub) + fmt.Printf("id.Hub.Info: %+v\n", id.Hub.Info) + fmt.Printf("id.Hub.Status: %+v\n", id.Hub.Status) + + // Maintenance is run in creation, so nothing should change now. + changed, err := id.MaintainAnnouncement(nil, false) + if err != nil { + t.Fatal(err) + } + if changed { + t.Error("unexpected change of announcement") + } + changed, err = id.MaintainStatus(nil, nil, nil, false) + if err != nil { + t.Fatal(err) + } + if changed { + t.Error("unexpected change of status") + } + + // Change lanes. + lanes := []*hub.Lane{ + { + ID: "A", + Capacity: 1, + Latency: 2, + }, + { + ID: "B", + Capacity: 3, + Latency: 4, + }, + { + ID: "C", + Capacity: 5, + Latency: 6, + }, + } + changed, err = id.MaintainStatus(lanes, new(int), nil, false) + if err != nil { + t.Fatal(err) + } + if !changed { + t.Error("status should have changed") + } + + // Change nothing. + changed, err = id.MaintainStatus(lanes, new(int), nil, false) + if err != nil { + t.Fatal(err) + } + if changed { + t.Error("unexpected change of status") + } + + // Exporting + _, err = id.ExportAnnouncement() + if err != nil { + t.Fatal(err) + } + _, err = id.ExportStatus() + if err != nil { + t.Fatal(err) + } + + // Save to and load from database. + err = id.Save() + if err != nil { + t.Fatal(err) + } + id2, changed, err := LoadIdentity(identityTestKey) + if err != nil { + t.Fatal(err) + } + if changed { + t.Error("unexpected change") + } + + // Check if they match + assert.Equal(t, id, id2, "identities should be equal") +} diff --git a/spn/cabin/keys.go b/spn/cabin/keys.go new file mode 100644 index 00000000..67d203a4 --- /dev/null +++ b/spn/cabin/keys.go @@ -0,0 +1,179 @@ +package cabin + +import ( + "encoding/base64" + "errors" + "fmt" + "time" + + "github.com/safing/jess" + "github.com/safing/jess/tools" + "github.com/safing/portbase/log" + "github.com/safing/portbase/rng" + "github.com/safing/portmaster/spn/hub" +) + +type providedExchKeyScheme struct { + id string + securityLevel int //nolint:structcheck // TODO + tool *tools.Tool +} + +var ( + // validFor defines how long keys are valid for use by clients. + validFor = 48 * time.Hour // 2 days + // renewBeforeExpiry defines the duration how long before expiry keys should be renewed. + renewBeforeExpiry = 24 * time.Hour // 1 day + + // burnAfter defines how long after expiry keys are burnt/deleted. + burnAfter = 12 * time.Hour // 1/2 day + // reuseAfter defines how long IDs should be blocked after expiry (and not be reused for new keys). + reuseAfter = 2 * 7 * 24 * time.Hour // 2 weeks + + // provideExchKeySchemes defines the jess tools for creating exchange keys. + provideExchKeySchemes = []*providedExchKeyScheme{ + { + id: "ECDH-X25519", + securityLevel: 128, // informative only, security level of ECDH-X25519 is fixed + }, + // TODO: test with rsa keys + } +) + +func initProvidedExchKeySchemes() error { + for _, eks := range provideExchKeySchemes { + tool, err := tools.Get(eks.id) + if err != nil { + return err + } + eks.tool = tool + } + return nil +} + +// MaintainExchKeys maintains the exchange keys, creating new ones and +// deprecating and deleting old ones. +func (id *Identity) MaintainExchKeys(newStatus *hub.Status, now time.Time) (changed bool, err error) { + // create Keys map + if id.ExchKeys == nil { + id.ExchKeys = make(map[string]*ExchKey) + } + + // lifecycle management + for keyID, exchKey := range id.ExchKeys { + if exchKey.key != nil && now.After(exchKey.Expires.Add(burnAfter)) { + // delete key + err := exchKey.tool.StaticLogic.BurnKey(exchKey.key) + if err != nil { + log.Warningf( + "spn/cabin: failed to burn key %s (%s) of %s: %s", + keyID, + exchKey.tool.Info.Name, + id.Hub.ID, + err, + ) + } + // remove reference + exchKey.key = nil + } + if now.After(exchKey.Expires.Add(reuseAfter)) { + // remove key + delete(id.ExchKeys, keyID) + } + } + + // find or create current keys + for _, eks := range provideExchKeySchemes { + found := false + for _, exchKey := range id.ExchKeys { + if exchKey.key != nil && + exchKey.key.Scheme == eks.id && + now.Before(exchKey.Expires.Add(-renewBeforeExpiry)) { + found = true + break + } + } + + if !found { + err := id.createExchKey(eks, now) + if err != nil { + return false, fmt.Errorf("failed to create %s exchange key: %w", eks.tool.Info.Name, err) + } + changed = true + } + } + + // export most recent keys to HubStatus + if changed || len(newStatus.Keys) == 0 { + // reset + newStatus.Keys = make(map[string]*hub.Key) + + // find longest valid key for every provided scheme + for _, eks := range provideExchKeySchemes { + // find key of scheme that is valid the longest + longestValid := &ExchKey{ + Expires: now, + } + for _, exchKey := range id.ExchKeys { + if exchKey.key != nil && + exchKey.key.Scheme == eks.id && + exchKey.Expires.After(longestValid.Expires) { + longestValid = exchKey + } + } + + // check result + if longestValid.key == nil { + log.Warningf("spn/cabin: could not find export candidate for exchange key scheme %s", eks.id) + continue + } + + // export + hubKey, err := longestValid.toHubKey() + if err != nil { + return false, fmt.Errorf("failed to export %s exchange key: %w", longestValid.tool.Info.Name, err) + } + // add + newStatus.Keys[longestValid.key.ID] = hubKey + } + } + + return changed, nil +} + +func (id *Identity) createExchKey(eks *providedExchKeyScheme, now time.Time) error { + // get ID + var keyID string + for i := 0; i < 1000000; i++ { // not forever + // generate new ID + b, err := rng.Bytes(3) + if err != nil { + return fmt.Errorf("failed to get random data for key ID: %w", err) + } + keyID = base64.RawURLEncoding.EncodeToString(b) + _, exists := id.ExchKeys[keyID] + if !exists { + break + } + } + if keyID == "" { + return errors.New("unable to find available exchange key ID") + } + + // generate key + signet := jess.NewSignetBase(eks.tool) + signet.ID = keyID + // TODO: use security level for key generation + if err := signet.GenerateKey(); err != nil { + return fmt.Errorf("failed to get new exchange key: %w", err) + } + + // add to key map + id.ExchKeys[keyID] = &ExchKey{ + Created: now, + Expires: now.Add(validFor), + key: signet, + tool: eks.tool, + } + return nil +} diff --git a/spn/cabin/keys_test.go b/spn/cabin/keys_test.go new file mode 100644 index 00000000..c1622fe6 --- /dev/null +++ b/spn/cabin/keys_test.go @@ -0,0 +1,43 @@ +package cabin + +import ( + "testing" + "time" + + "github.com/safing/portmaster/spn/conf" +) + +func TestKeyMaintenance(t *testing.T) { + t.Parallel() + + id, err := CreateIdentity(module.Ctx, conf.MainMapName) + if err != nil { + t.Fatal(err) + } + + iterations := 1000 + changeCnt := 0 + + now := time.Now() + for i := 0; i < iterations; i++ { + changed, err := id.MaintainExchKeys(id.Hub.Status, now) + if err != nil { + t.Fatal(err) + } + if changed { + changeCnt++ + t.Logf("===== exchange keys updated at %s:\n", now) + for keyID, exchKey := range id.ExchKeys { + t.Logf("[%s] %s %v\n", exchKey.Created, keyID, exchKey.key) + } + } + now = now.Add(1 * time.Hour) + } + + if iterations/changeCnt > 25 { // one new key every 24 hours/ticks + t.Fatal("more changes than expected") + } + if len(id.ExchKeys) > 17 { // one new key every day for two weeks + 3 in use + t.Fatal("more keys than expected") + } +} diff --git a/spn/cabin/module.go b/spn/cabin/module.go new file mode 100644 index 00000000..8644502f --- /dev/null +++ b/spn/cabin/module.go @@ -0,0 +1,26 @@ +package cabin + +import ( + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/spn/conf" +) + +var module *modules.Module + +func init() { + module = modules.Register("cabin", prep, nil, nil, "base", "rng") +} + +func prep() error { + if err := initProvidedExchKeySchemes(); err != nil { + return err + } + + if conf.PublicHub() { + if err := prepPublicHubConfig(); err != nil { + return err + } + } + + return nil +} diff --git a/spn/cabin/module_test.go b/spn/cabin/module_test.go new file mode 100644 index 00000000..c2d66ed1 --- /dev/null +++ b/spn/cabin/module_test.go @@ -0,0 +1,13 @@ +package cabin + +import ( + "testing" + + "github.com/safing/portmaster/service/core/pmtesting" + "github.com/safing/portmaster/spn/conf" +) + +func TestMain(m *testing.M) { + conf.EnablePublicHub(true) + pmtesting.TestMain(m, module) +} diff --git a/spn/cabin/verification.go b/spn/cabin/verification.go new file mode 100644 index 00000000..07a993ea --- /dev/null +++ b/spn/cabin/verification.go @@ -0,0 +1,157 @@ +package cabin + +import ( + "crypto/subtle" + "errors" + "fmt" + + "github.com/safing/jess" + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portbase/rng" + "github.com/safing/portmaster/spn/hub" +) + +var ( + verificationChallengeSize = 32 + verificationChallengeMinSize = 16 + verificationSigningSuite = jess.SuiteSignV1 + verificationRequirements = jess.NewRequirements(). + Remove(jess.Confidentiality). + Remove(jess.Integrity). + Remove(jess.RecipientAuthentication) +) + +// Verification is used to verify certain aspects of another Hub. +type Verification struct { + // Challenge is a random value chosen by the client. + Challenge []byte `json:"c"` + // Purpose defines the purpose of the verification. Protects against using verification for other purposes. + Purpose string `json:"p"` + // ClientReference is an optional field for exchanging metadata about the client. Protects against forwarding/relay attacks. + ClientReference string `json:"cr"` + // ServerReference is an optional field for exchanging metadata about the server. Protects against forwarding/relay attacks. + ServerReference string `json:"sr"` +} + +// CreateVerificationRequest creates a new verification request with the given +// purpose and references. +func CreateVerificationRequest(purpose, clientReference, serverReference string) (v *Verification, request []byte, err error) { + // Generate random challenge. + challenge, err := rng.Bytes(verificationChallengeSize) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate challenge: %w", err) + } + + // Create verification object. + v = &Verification{ + Purpose: purpose, + ClientReference: clientReference, + Challenge: challenge, + } + + // Serialize verification. + request, err = dsd.Dump(v, dsd.JSON) + if err != nil { + return nil, nil, fmt.Errorf("failed to serialize verification request: %w", err) + } + + // The server reference is not sent to the server, but needs to be supplied + // by the server. + v.ServerReference = serverReference + + return v, request, nil +} + +// SignVerificationRequest sign a verification request. +// The purpose and references must match the request, else the verification +// will fail. +func (id *Identity) SignVerificationRequest(request []byte, purpose, clientReference, serverReference string) (response []byte, err error) { + // Parse request. + v := new(Verification) + _, err = dsd.Load(request, v) + if err != nil { + return nil, fmt.Errorf("failed to parse request: %w", err) + } + + // Validate request. + if len(v.Challenge) < verificationChallengeMinSize { + return nil, errors.New("challenge too small") + } + if v.Purpose != purpose { + return nil, errors.New("purpose mismatch") + } + if v.ClientReference != clientReference { + return nil, errors.New("client reference mismatch") + } + + // Assign server reference and serialize. + v.ServerReference = serverReference + dataToSign, err := dsd.Dump(v, dsd.JSON) + if err != nil { + return nil, fmt.Errorf("failed to serialize verification response: %w", err) + } + + // Sign response. + e := jess.NewUnconfiguredEnvelope() + e.SuiteID = verificationSigningSuite + e.Senders = []*jess.Signet{id.Signet} + jession, err := e.Correspondence(nil) + if err != nil { + return nil, fmt.Errorf("failed to setup signer: %w", err) + } + letter, err := jession.Close(dataToSign) + if err != nil { + return nil, fmt.Errorf("failed to sign: %w", err) + } + + // Serialize and return. + signedResponse, err := letter.ToDSD(dsd.JSON) + if err != nil { + return nil, fmt.Errorf("failed to serialize letter: %w", err) + } + + return signedResponse, nil +} + +// Verify verifies the verification response and checks if everything is valid. +func (v *Verification) Verify(response []byte, h *hub.Hub) error { + // Parse response. + letter, err := jess.LetterFromDSD(response) + if err != nil { + return fmt.Errorf("failed to parse response: %w", err) + } + + // Verify response. + responseData, err := letter.Open( + verificationRequirements, + &hub.SingleTrustStore{ + Signet: h.PublicKey, + }, + ) + if err != nil { + return fmt.Errorf("failed to verify response: %w", err) + } + + // Parse verified response. + responseV := new(Verification) + _, err = dsd.Load(responseData, responseV) + if err != nil { + return fmt.Errorf("failed to parse verified response: %w", err) + } + + // Validate request. + if subtle.ConstantTimeCompare(v.Challenge, responseV.Challenge) != 1 { + return errors.New("challenge mismatch") + } + if subtle.ConstantTimeCompare([]byte(v.Purpose), []byte(responseV.Purpose)) != 1 { + return errors.New("purpose mismatch") + } + if subtle.ConstantTimeCompare([]byte(v.ClientReference), []byte(responseV.ClientReference)) != 1 { + return errors.New("client reference mismatch") + } + if subtle.ConstantTimeCompare([]byte(v.ServerReference), []byte(responseV.ServerReference)) != 1 { + return errors.New("server reference mismatch") + } + + return nil +} diff --git a/spn/cabin/verification_test.go b/spn/cabin/verification_test.go new file mode 100644 index 00000000..cb743a3d --- /dev/null +++ b/spn/cabin/verification_test.go @@ -0,0 +1,127 @@ +package cabin + +import ( + "fmt" + "testing" +) + +func TestVerification(t *testing.T) { + t.Parallel() + + id, err := CreateIdentity(module.Ctx, "test") + if err != nil { + t.Fatal(err) + } + + if err := testVerificationWith( + t, id, + "a", "b", "c", + "a", "b", "c", + "", "", "", nil, + ); err != nil { + t.Fatal(err) + } + + if err := testVerificationWith( + t, id, + "a", "b", "c", + "x", "b", "c", + "", "", "", nil, + ); err == nil { + t.Fatal("should fail on purpose mismatch") + } + + if err := testVerificationWith( + t, id, + "a", "b", "c", + "a", "x", "c", + "", "", "", nil, + ); err == nil { + t.Fatal("should fail on client ref mismatch") + } + + if err := testVerificationWith( + t, id, + "a", "b", "c", + "a", "b", "x", + "", "", "", nil, + ); err == nil { + t.Fatal("should fail on server ref mismatch") + } + + if err := testVerificationWith( + t, id, + "a", "b", "c", + "a", "b", "c", + "x", "", "", nil, + ); err == nil { + t.Fatal("should fail on purpose mismatch") + } + + if err := testVerificationWith( + t, id, + "a", "b", "c", + "a", "b", "c", + "", "x", "", nil, + ); err == nil { + t.Fatal("should fail on client ref mismatch") + } + + if err := testVerificationWith( + t, id, + "a", "b", "c", + "a", "b", "c", + "", "", "x", nil, + ); err == nil { + t.Fatal("should fail on server ref mismatch") + } + + if err := testVerificationWith( + t, id, + "a", "b", "c", + "a", "b", "c", + "", "", "", []byte{1, 2, 3, 4}, + ); err == nil { + t.Fatal("should fail on challenge mismatch") + } +} + +func testVerificationWith( + t *testing.T, id *Identity, + purpose1, clientRef1, serverRef1 string, //nolint:unparam + purpose2, clientRef2, serverRef2 string, + mitmPurpose, mitmClientRef, mitmServerRef string, + mitmChallenge []byte, +) error { + t.Helper() + + v, request, err := CreateVerificationRequest(purpose1, clientRef1, serverRef1) + if err != nil { + return fmt.Errorf("failed to create verification request: %w", err) + } + + response, err := id.SignVerificationRequest(request, purpose2, clientRef2, serverRef2) + if err != nil { + return fmt.Errorf("failed to sign verification response: %w", err) + } + + if mitmPurpose != "" { + v.Purpose = mitmPurpose + } + if mitmClientRef != "" { + v.ClientReference = mitmClientRef + } + if mitmServerRef != "" { + v.ServerReference = mitmServerRef + } + if mitmChallenge != nil { + v.Challenge = mitmChallenge + } + + err = v.Verify(response, id.Hub) + if err != nil { + return fmt.Errorf("failed to verify: %w", err) + } + + return nil +} diff --git a/spn/captain/api.go b/spn/captain/api.go new file mode 100644 index 00000000..dcc412d8 --- /dev/null +++ b/spn/captain/api.go @@ -0,0 +1,68 @@ +package captain + +import ( + "errors" + "fmt" + + "github.com/safing/portbase/api" + "github.com/safing/portbase/database" + "github.com/safing/portbase/database/query" + "github.com/safing/portbase/modules" +) + +const ( + apiPathForSPNReInit = "spn/reinit" +) + +func registerAPIEndpoints() error { + if err := api.RegisterEndpoint(api.Endpoint{ + Path: apiPathForSPNReInit, + Write: api.PermitAdmin, + // BelongsTo: module, // Do not attach to module, as this must run outside of the module. + ActionFunc: handleReInit, + Name: "Re-initialize SPN", + Description: "Stops the SPN, resets all caches and starts it again. The SPN account and settings are not changed.", + }); err != nil { + return err + } + + return nil +} + +func handleReInit(ar *api.Request) (msg string, err error) { + // Disable module and check + changed := module.Disable() + if !changed { + return "", errors.New("can only re-initialize when the SPN is enabled") + } + + // Run module manager. + err = modules.ManageModules() + if err != nil { + return "", fmt.Errorf("failed to stop SPN: %w", err) + } + + // Delete SPN cache. + db := database.NewInterface(&database.Options{ + Local: true, + Internal: true, + }) + deletedRecords, err := db.Purge(ar.Context(), query.New("cache:spn/")) + if err != nil { + return "", fmt.Errorf("failed to delete SPN cache: %w", err) + } + + // Enable module. + module.Enable() + + // Run module manager. + err = modules.ManageModules() + if err != nil { + return "", fmt.Errorf("failed to start SPN after cache reset: %w", err) + } + + return fmt.Sprintf( + "Completed SPN re-initialization and deleted %d cache records in the process.", + deletedRecords, + ), nil +} diff --git a/spn/captain/bootstrap.go b/spn/captain/bootstrap.go new file mode 100644 index 00000000..c7096116 --- /dev/null +++ b/spn/captain/bootstrap.go @@ -0,0 +1,152 @@ +package captain + +import ( + "errors" + "flag" + "fmt" + "io/fs" + "os" + + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/navigator" +) + +// BootstrapFile is used for sideloading bootstrap data. +type BootstrapFile struct { + Main BootstrapFileEntry +} + +// BootstrapFileEntry is the bootstrap data structure for one map. +type BootstrapFileEntry struct { + Hubs []string +} + +var ( + bootstrapHubFlag string + bootstrapFileFlag string +) + +func init() { + flag.StringVar(&bootstrapHubFlag, "bootstrap-hub", "", "transport address of hub for bootstrapping with the hub ID in the fragment") + flag.StringVar(&bootstrapFileFlag, "bootstrap-file", "", "bootstrap file containing bootstrap hubs - will be initialized if running a public hub and it doesn't exist") +} + +// prepBootstrapHubFlag checks the bootstrap-hub argument if it is valid. +func prepBootstrapHubFlag() error { + if bootstrapHubFlag != "" { + _, _, _, err := hub.ParseBootstrapHub(bootstrapHubFlag) + return err + } + return nil +} + +// processBootstrapHubFlag processes the bootstrap-hub argument. +func processBootstrapHubFlag() error { + if bootstrapHubFlag != "" { + return navigator.Main.AddBootstrapHubs([]string{bootstrapHubFlag}) + } + return nil +} + +// processBootstrapFileFlag processes the bootstrap-file argument. +func processBootstrapFileFlag() error { + if bootstrapFileFlag == "" { + return nil + } + + _, err := os.Stat(bootstrapFileFlag) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return createBootstrapFile(bootstrapFileFlag) + } + return fmt.Errorf("failed to access bootstrap hub file: %w", err) + } + + return loadBootstrapFile(bootstrapFileFlag) +} + +// bootstrapWithUpdates loads bootstrap hubs from the updates server and imports them. +func bootstrapWithUpdates() error { + if bootstrapFileFlag != "" { + return errors.New("using the bootstrap-file argument disables bootstrapping via the update system") + } + + return updateSPNIntel(module.Ctx, nil) +} + +// loadBootstrapFile loads a file with bootstrap hub entries and imports them. +func loadBootstrapFile(filename string) (err error) { + // Load bootstrap file from disk and parse it. + data, err := os.ReadFile(filename) + if err != nil { + return fmt.Errorf("failed to load bootstrap file: %w", err) + } + bootstrapFile := &BootstrapFile{} + _, err = dsd.Load(data, bootstrapFile) + if err != nil { + return fmt.Errorf("failed to parse bootstrap file: %w", err) + } + if len(bootstrapFile.Main.Hubs) == 0 { + return errors.New("bootstrap holds no hubs for main map") + } + + // Add Hubs to map. + err = navigator.Main.AddBootstrapHubs(bootstrapFile.Main.Hubs) + if err == nil { + log.Infof("spn/captain: loaded bootstrap file %s", filename) + } + return err +} + +// createBootstrapFile save a bootstrap hub file with an entry of the public identity. +func createBootstrapFile(filename string) error { + if !conf.PublicHub() { + log.Infof("spn/captain: skipped writing a bootstrap hub file, as this is not a public hub") + return nil + } + + // create bootstrap hub + if len(publicIdentity.Hub.Info.Transports) == 0 { + return errors.New("public identity has no transports available") + } + // parse first transport + t, err := hub.ParseTransport(publicIdentity.Hub.Info.Transports[0]) + if err != nil { + return fmt.Errorf("failed to parse transport of public identity: %w", err) + } + // add IP address + switch { + case publicIdentity.Hub.Info.IPv4 != nil: + t.Domain = publicIdentity.Hub.Info.IPv4.String() + case publicIdentity.Hub.Info.IPv6 != nil: + t.Domain = "[" + publicIdentity.Hub.Info.IPv6.String() + "]" + default: + return errors.New("public identity has no IP address available") + } + // add Hub ID + t.Option = publicIdentity.Hub.ID + // put together + bs := &BootstrapFile{ + Main: BootstrapFileEntry{ + Hubs: []string{t.String()}, + }, + } + + // serialize + fileData, err := dsd.Dump(bs, dsd.JSON) + if err != nil { + return err + } + + // save to disk + err = os.WriteFile(filename, fileData, 0o0664) //nolint:gosec // Should be able to be read by others. + if err != nil { + return err + } + + log.Infof("spn/captain: created bootstrap file %s", filename) + return nil +} diff --git a/spn/captain/client.go b/spn/captain/client.go new file mode 100644 index 00000000..b30e4e98 --- /dev/null +++ b/spn/captain/client.go @@ -0,0 +1,506 @@ +package captain + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/tevino/abool" + + "github.com/safing/portbase/log" + "github.com/safing/portbase/notifications" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/spn/access" + "github.com/safing/portmaster/spn/crew" + "github.com/safing/portmaster/spn/docks" + "github.com/safing/portmaster/spn/navigator" + "github.com/safing/portmaster/spn/terminal" +) + +var ( + ready = abool.New() + + spnLoginButton = notifications.Action{ + Text: "Login", + Type: notifications.ActionTypeOpenPage, + Payload: "spn", + } + spnOpenAccountPage = notifications.Action{ + Text: "Open Account Page", + Type: notifications.ActionTypeOpenURL, + Payload: "https://account.safing.io", + } +) + +// ClientReady signifies if the SPN client is fully ready to handle connections. +func ClientReady() bool { + return ready.IsSet() +} + +type ( + clientComponentFunc func(ctx context.Context) clientComponentResult + clientComponentResult uint8 +) + +const ( + clientResultOk clientComponentResult = iota // Continue and clean module status. + clientResultRetry // Go back to start of current step, don't clear module status. + clientResultReconnect // Stop current connection and start from zero. + clientResultShutdown // SPN Module is shutting down. +) + +var ( + clientNetworkChangedFlag = netenv.GetNetworkChangedFlag() + clientIneligibleAccountUpdateDelay = 1 * time.Minute + clientRetryConnectBackoffDuration = 5 * time.Second + clientInitialHealthCheckDelay = 10 * time.Second + clientHealthCheckTickDuration = 1 * time.Minute + clientHealthCheckTickDurationSleepMode = 5 * time.Minute + clientHealthCheckTimeout = 15 * time.Second + + clientHealthCheckTrigger = make(chan struct{}, 1) + lastHealthCheck time.Time +) + +func triggerClientHealthCheck() { + select { + case clientHealthCheckTrigger <- struct{}{}: + default: + } +} + +func clientManager(ctx context.Context) error { + defer func() { + ready.UnSet() + netenv.ConnectedToSPN.UnSet() + resetSPNStatus(StatusDisabled, true) + module.Resolve("") + clientStopHomeHub(ctx) + }() + + module.Hint( + "spn:establishing-home-hub", + "Connecting to SPN...", + "Connecting to the SPN network is in progress.", + ) + + // TODO: When we are starting and the SPN module is faster online than the + // nameserver, then updating the account will fail as the DNS query is + // redirected to a closed port. + // We also can't add the nameserver as a module dependency, as the nameserver + // is not part of the server. + select { + case <-time.After(1 * time.Second): + case <-ctx.Done(): + return nil + } + + healthCheckTicker := module.NewSleepyTicker(clientHealthCheckTickDuration, clientHealthCheckTickDurationSleepMode) + +reconnect: + for { + // Check if we are shutting down. + select { + case <-ctx.Done(): + return nil + default: + } + + // Reset SPN status. + if ready.SetToIf(true, false) { + netenv.ConnectedToSPN.UnSet() + log.Info("spn/captain: client not ready") + } + resetSPNStatus(StatusConnecting, true) + + // Check everything and connect to the SPN. + for _, clientFunc := range []clientComponentFunc{ + clientStopHomeHub, + clientCheckNetworkReady, + clientCheckAccountAndTokens, + clientConnectToHomeHub, + clientSetActiveConnectionStatus, + } { + switch clientFunc(ctx) { + case clientResultOk: + // Continue + case clientResultRetry, clientResultReconnect: + // Wait for a short time to not loop too quickly. + select { + case <-time.After(clientRetryConnectBackoffDuration): + continue reconnect + case <-ctx.Done(): + return nil + } + case clientResultShutdown: + return nil + } + } + + log.Info("spn/captain: client is ready") + ready.Set() + netenv.ConnectedToSPN.Set() + + module.TriggerEvent(SPNConnectedEvent, nil) + module.StartWorker("update quick setting countries", navigator.Main.UpdateConfigQuickSettings) + + // Reset last health check value, as we have just connected. + lastHealthCheck = time.Now() + + // Back off before starting initial health checks. + select { + case <-time.After(clientInitialHealthCheckDelay): + case <-ctx.Done(): + return nil + } + + for { + // Check health of the current SPN connection and monitor the user status. + maintainers: + for _, clientFunc := range []clientComponentFunc{ + clientCheckHomeHubConnection, + clientCheckAccountAndTokens, + clientSetActiveConnectionStatus, + } { + switch clientFunc(ctx) { + case clientResultOk: + // Continue + case clientResultRetry: + // Abort and wait for the next run. + break maintainers + case clientResultReconnect: + continue reconnect + case clientResultShutdown: + return nil + } + } + + // Wait for signal to run maintenance again. + select { + case <-healthCheckTicker.Wait(): + case <-clientHealthCheckTrigger: + case <-crew.ConnectErrors(): + case <-clientNetworkChangedFlag.Signal(): + clientNetworkChangedFlag.Refresh() + case <-ctx.Done(): + return nil + } + } + } +} + +func clientCheckNetworkReady(ctx context.Context) clientComponentResult { + // Check if we are online enough for connecting. + switch netenv.GetOnlineStatus() { //nolint:exhaustive + case netenv.StatusOffline, + netenv.StatusLimited: + select { + case <-ctx.Done(): + return clientResultShutdown + case <-time.After(1 * time.Second): + return clientResultRetry + } + } + + return clientResultOk +} + +// DisableAccount disables using any account related SPN functionality. +// Attempts to use the same will result in errors. +var DisableAccount bool + +func clientCheckAccountAndTokens(ctx context.Context) clientComponentResult { + if DisableAccount { + return clientResultOk + } + + // Get SPN user. + user, err := access.GetUser() + if err != nil && !errors.Is(err, access.ErrNotLoggedIn) { + notifications.NotifyError( + "spn:failed-to-get-user", + "SPN Internal Error", + `Please restart Portmaster.`, + // TODO: Add restart button. + // TODO: Use special UI restart action in order to reload UI on restart. + ).AttachToModule(module) + resetSPNStatus(StatusFailed, true) + log.Errorf("spn/captain: client internal error: %s", err) + return clientResultReconnect + } + + // Check if user is logged in. + if user == nil || !user.IsLoggedIn() { + notifications.NotifyWarn( + "spn:not-logged-in", + "SPN Login Required", + `Please log in to access the SPN.`, + spnLoginButton, + ).AttachToModule(module) + resetSPNStatus(StatusFailed, true) + log.Warningf("spn/captain: enabled but not logged in") + return clientResultReconnect + } + + // Check if user is eligible. + if !user.MayUseTheSPN() { + // Update user in case there was a change. + // Only update here if we need to - there is an update task in the access + // module for periodic updates. + if time.Now().Add(-clientIneligibleAccountUpdateDelay).After(time.Unix(user.Meta().Modified, 0)) { + _, _, err := access.UpdateUser() + if err != nil { + notifications.NotifyError( + "spn:failed-to-update-user", + "SPN Account Server Error", + fmt.Sprintf(`The status of your SPN account could not be updated: %s`, err), + ).AttachToModule(module) + resetSPNStatus(StatusFailed, true) + log.Errorf("spn/captain: failed to update ineligible account: %s", err) + return clientResultReconnect + } + } + + // Check if user is eligible after a possible update. + if !user.MayUseTheSPN() { + + // If package is generally valid, then the current package does not have access to the SPN. + if user.MayUse("") { + notifications.NotifyError( + "spn:package-not-eligible", + "SPN Not Included In Package", + "Your current Portmaster Package does not include access to the SPN. Please upgrade your package on the Account Page.", + spnOpenAccountPage, + ).AttachToModule(module) + resetSPNStatus(StatusFailed, true) + return clientResultReconnect + } + + // Otherwise, include the message from the user view. + message := "There is an issue with your Portmaster Package. Please check the Account Page." + if user.View != nil && user.View.Message != "" { + message = user.View.Message + } + notifications.NotifyError( + "spn:subscription-inactive", + "Portmaster Package Issue", + "Cannot enable SPN: "+message, + spnOpenAccountPage, + ).AttachToModule(module) + resetSPNStatus(StatusFailed, true) + return clientResultReconnect + } + } + + // Check if we have enough tokens. + if access.ShouldRequest(access.ExpandAndConnectZones) { + err := access.UpdateTokens() + if err != nil { + log.Errorf("spn/captain: failed to get tokens: %s", err) + + // There was an error updating the account. + // Check if we have enough tokens to continue anyway. + regular, _ := access.GetTokenAmount(access.ExpandAndConnectZones) + if regular == 0 /* && fallback == 0 */ { // TODO: Add fallback token check when fallback was tested on servers. + notifications.NotifyError( + "spn:tokens-exhausted", + "SPN Access Tokens Exhausted", + `The Portmaster failed to get new access tokens to access the SPN. The Portmaster will automatically retry to get new access tokens.`, + ).AttachToModule(module) + resetSPNStatus(StatusFailed, false) + } + return clientResultRetry + } + } + + return clientResultOk +} + +func clientStopHomeHub(ctx context.Context) clientComponentResult { + // Don't use the context in this function, as it will likely be canceled + // already and would disrupt any context usage in here. + + // Get crane connecting to home. + home, _ := navigator.Main.GetHome() + if home == nil { + return clientResultOk + } + crane := docks.GetAssignedCrane(home.Hub.ID) + if crane == nil { + return clientResultOk + } + + // Stop crane and all connected terminals. + crane.Stop(nil) + return clientResultOk +} + +func clientConnectToHomeHub(ctx context.Context) clientComponentResult { + err := establishHomeHub(ctx) + if err != nil { + log.Errorf("spn/captain: failed to establish connection to home hub: %s", err) + resetSPNStatus(StatusFailed, true) + + switch { + case errors.Is(err, ErrAllHomeHubsExcluded): + notifications.NotifyError( + "spn:all-home-hubs-excluded", + "All Home Nodes Excluded", + "Your current Home Node Rules exclude all available and eligible SPN Nodes. Please change your rules to allow for at least one available and eligible Home Node.", + notifications.Action{ + Text: "Configure", + Type: notifications.ActionTypeOpenSetting, + Payload: ¬ifications.ActionTypeOpenSettingPayload{ + Key: CfgOptionHomeHubPolicyKey, + }, + }, + ).AttachToModule(module) + + case errors.Is(err, ErrReInitSPNSuggested): + notifications.NotifyError( + "spn:cannot-bootstrap", + "SPN Cannot Bootstrap", + "The local state of the SPN network is likely outdated. Portmaster was not able to identify a server to connect to. Please re-initialize the SPN using the tools menu or the button on the notification.", + notifications.Action{ + ID: "re-init", + Text: "Re-Init SPN", + Type: notifications.ActionTypeWebhook, + Payload: ¬ifications.ActionTypeWebhookPayload{ + URL: apiPathForSPNReInit, + ResultAction: "display", + }, + }, + ).AttachToModule(module) + + default: + notifications.NotifyWarn( + "spn:home-hub-failure", + "SPN Failed to Connect", + fmt.Sprintf("Failed to connect to a home hub: %s. The Portmaster will retry to connect automatically.", err), + ).AttachToModule(module) + } + + return clientResultReconnect + } + + // Log new connection. + home, _ := navigator.Main.GetHome() + if home != nil { + log.Infof("spn/captain: established new home %s", home.Hub) + } + + return clientResultOk +} + +func clientSetActiveConnectionStatus(ctx context.Context) clientComponentResult { + // Get current home. + home, homeTerminal := navigator.Main.GetHome() + if home == nil || homeTerminal == nil { + return clientResultReconnect + } + + // Resolve any connection error. + module.Resolve("") + + // Update SPN Status with connection information, if not already correctly set. + spnStatus.Lock() + defer spnStatus.Unlock() + + if spnStatus.Status != StatusConnected || spnStatus.HomeHubID != home.Hub.ID { + // Fill connection status data. + spnStatus.Status = StatusConnected + spnStatus.HomeHubID = home.Hub.ID + spnStatus.HomeHubName = home.Hub.Info.Name + + connectedIP, _, err := netutils.IPPortFromAddr(homeTerminal.RemoteAddr()) + if err != nil { + spnStatus.ConnectedIP = homeTerminal.RemoteAddr().String() + } else { + spnStatus.ConnectedIP = connectedIP.String() + } + spnStatus.ConnectedTransport = homeTerminal.Transport().String() + + geoLoc := home.GetLocation(connectedIP) + if geoLoc != nil { + spnStatus.ConnectedCountry = &geoLoc.Country + } + + now := time.Now() + spnStatus.ConnectedSince = &now + + // Push new status. + pushSPNStatusUpdate() + } + + return clientResultOk +} + +func clientCheckHomeHubConnection(ctx context.Context) clientComponentResult { + // Check the status of the Home Hub. + home, homeTerminal := navigator.Main.GetHome() + if home == nil || homeTerminal == nil || homeTerminal.IsBeingAbandoned() { + return clientResultReconnect + } + + // Get crane controller for health check. + crane := docks.GetAssignedCrane(home.Hub.ID) + if crane == nil { + log.Errorf("spn/captain: could not find home hub crane for health check") + return clientResultOk + } + + // Ping home hub. + latency, tErr := pingHome(ctx, crane.Controller, clientHealthCheckTimeout) + if tErr != nil { + log.Warningf("spn/captain: failed to ping home hub: %s", tErr) + + // Prepare to reconnect to the network. + + // Reset all failing states, as these might have been caused by the failing home hub. + navigator.Main.ResetFailingStates(ctx) + + // If the last health check is clearly too long ago, assume that the device was sleeping and do not set the home node to failing yet. + if time.Since(lastHealthCheck) > clientHealthCheckTickDuration+ + clientHealthCheckTickDurationSleepMode+ + (clientHealthCheckTimeout*2) { + return clientResultReconnect + } + + // Mark the home hub itself as failing, as we want to try to connect to somewhere else. + home.MarkAsFailingFor(5 * time.Minute) + + return clientResultReconnect + } + lastHealthCheck = time.Now() + + log.Debugf("spn/captain: pinged home hub in %s", latency) + return clientResultOk +} + +func pingHome(ctx context.Context, t terminal.Terminal, timeout time.Duration) (latency time.Duration, err *terminal.Error) { + started := time.Now() + + // Start ping operation. + pingOp, tErr := crew.NewPingOp(t) + if tErr != nil { + return 0, tErr + } + + // Wait for response. + select { + case <-ctx.Done(): + return 0, terminal.ErrCanceled + case <-time.After(timeout): + return 0, terminal.ErrTimeout + case result := <-pingOp.Result: + if result.Is(terminal.ErrExplicitAck) { + return time.Since(started), nil + } + if result.IsOK() { + return 0, result.Wrap("unexpected response") + } + return 0, result + } +} diff --git a/spn/captain/config.go b/spn/captain/config.go new file mode 100644 index 00000000..09e6f490 --- /dev/null +++ b/spn/captain/config.go @@ -0,0 +1,253 @@ +package captain + +import ( + "sync" + + "github.com/safing/portbase/config" + "github.com/safing/portmaster/service/profile" + "github.com/safing/portmaster/service/profile/endpoints" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/navigator" +) + +var ( + // CfgOptionEnableSPNKey is the configuration key for the SPN module. + CfgOptionEnableSPNKey = "spn/enable" + cfgOptionEnableSPNOrder = 128 + + // CfgOptionHomeHubPolicyKey is the configuration key for the SPN home policy. + CfgOptionHomeHubPolicyKey = "spn/homePolicy" + cfgOptionHomeHubPolicy config.StringArrayOption + cfgOptionHomeHubPolicyOrder = 145 + + // CfgOptionDNSExitHubPolicyKey is the configuration key for the SPN DNS exit policy. + CfgOptionDNSExitHubPolicyKey = "spn/dnsExitPolicy" + cfgOptionDNSExitHubPolicy config.StringArrayOption + cfgOptionDNSExitHubPolicyOrder = 148 + + // CfgOptionUseCommunityNodesKey is the configuration key for whether to use community nodes. + CfgOptionUseCommunityNodesKey = "spn/useCommunityNodes" + cfgOptionUseCommunityNodes config.BoolOption + cfgOptionUseCommunityNodesOrder = 149 + + // NonCommunityVerifiedOwners holds a list of verified owners that are not + // considered "community". + NonCommunityVerifiedOwners = []string{"Safing"} + + // CfgOptionTrustNodeNodesKey is the configuration key for whether additional trusted nodes. + CfgOptionTrustNodeNodesKey = "spn/trustNodes" + cfgOptionTrustNodeNodes config.StringArrayOption + cfgOptionTrustNodeNodesOrder = 150 + + // Special Access Code. + cfgOptionSpecialAccessCodeKey = "spn/specialAccessCode" + cfgOptionSpecialAccessCodeDefault = "none" + cfgOptionSpecialAccessCode config.StringOption //nolint:unused // Linter, you drunk? + cfgOptionSpecialAccessCodeOrder = 160 + + // IPv6 must be global and accessible. + cfgOptionBindToAdvertisedKey = "spn/publicHub/bindToAdvertised" + cfgOptionBindToAdvertised config.BoolOption + cfgOptionBindToAdvertisedDefault = false + cfgOptionBindToAdvertisedOrder = 161 + + // Config options for use. + cfgOptionRoutingAlgorithm config.StringOption +) + +func prepConfig() error { + // Home Node Rules + err := config.Register(&config.Option{ + Name: "Home Node Rules", + Key: CfgOptionHomeHubPolicyKey, + Description: `Customize which countries should or should not be used for your Home Node. The Home Node is your entry into the SPN. You connect directly to it and all your connections are routed through it. + +By default, the Portmaster tries to choose the nearest node as your Home Node in order to reduce your exposure to the open Internet. + +Reconnect to the SPN in order to apply new rules.`, + Help: profile.SPNRulesHelp, + Sensitive: true, + OptType: config.OptTypeStringArray, + RequiresRestart: true, + ExpertiseLevel: config.ExpertiseLevelExpert, + DefaultValue: []string{}, + Annotations: config.Annotations{ + config.CategoryAnnotation: "Routing", + config.DisplayOrderAnnotation: cfgOptionHomeHubPolicyOrder, + config.DisplayHintAnnotation: endpoints.DisplayHintEndpointList, + config.QuickSettingsAnnotation: profile.SPNRulesQuickSettings, + endpoints.EndpointListVerdictNamesAnnotation: profile.SPNRulesVerdictNames, + }, + ValidationRegex: endpoints.ListEntryValidationRegex, + ValidationFunc: endpoints.ValidateEndpointListConfigOption, + }) + if err != nil { + return err + } + cfgOptionHomeHubPolicy = config.Concurrent.GetAsStringArray(CfgOptionHomeHubPolicyKey, []string{}) + + // DNS Exit Node Rules + err = config.Register(&config.Option{ + Name: "DNS Exit Node Rules", + Key: CfgOptionDNSExitHubPolicyKey, + Description: `Customize which countries should or should not be used as DNS Exit Nodes. + +By default, the Portmaster will exit DNS requests directly at your Home Node in order to keep them fast and close to your location. This is important, as DNS resolution often takes your approximate location into account when deciding which optimized DNS records are returned to you. As the Portmaster encrypts your DNS requests by default, you effectively gain a two-hop security level for your DNS requests in order to protect your privacy. + +This setting mainly exists for when you need to simulate your presence in another location on a lower level too. This might be necessary to defeat more intelligent geo-blocking systems.`, + Help: profile.SPNRulesHelp, + Sensitive: true, + OptType: config.OptTypeStringArray, + RequiresRestart: true, + ExpertiseLevel: config.ExpertiseLevelExpert, + DefaultValue: []string{}, + Annotations: config.Annotations{ + config.CategoryAnnotation: "Routing", + config.DisplayOrderAnnotation: cfgOptionDNSExitHubPolicyOrder, + config.DisplayHintAnnotation: endpoints.DisplayHintEndpointList, + config.QuickSettingsAnnotation: profile.SPNRulesQuickSettings, + endpoints.EndpointListVerdictNamesAnnotation: profile.SPNRulesVerdictNames, + }, + ValidationRegex: endpoints.ListEntryValidationRegex, + ValidationFunc: endpoints.ValidateEndpointListConfigOption, + }) + if err != nil { + return err + } + cfgOptionDNSExitHubPolicy = config.Concurrent.GetAsStringArray(CfgOptionDNSExitHubPolicyKey, []string{}) + + err = config.Register(&config.Option{ + Name: "Use Community Nodes", + Key: CfgOptionUseCommunityNodesKey, + Description: "Use nodes (servers) not operated by Safing themselves. The use of community nodes is recommended as it diversifies the ownership of the nodes you use for your connections and further strengthens your privacy. Plain connections (eg. http, smtp, ...) will never exit via community nodes, making this setting safe to use.", + Sensitive: true, + OptType: config.OptTypeBool, + RequiresRestart: true, + DefaultValue: true, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: cfgOptionUseCommunityNodesOrder, + config.CategoryAnnotation: "Routing", + }, + }) + if err != nil { + return err + } + cfgOptionUseCommunityNodes = config.Concurrent.GetAsBool(CfgOptionUseCommunityNodesKey, true) + + err = config.Register(&config.Option{ + Name: "Trust Nodes", + Key: CfgOptionTrustNodeNodesKey, + Description: "Specify which community nodes to additionally trust. These nodes may then also be used as a Home Node, as well as an Exit Node for unencrypted connections.", + Help: "You can specify nodes by their ID or their verified operator.", + Sensitive: true, + OptType: config.OptTypeStringArray, + ExpertiseLevel: config.ExpertiseLevelExpert, + DefaultValue: []string{}, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: cfgOptionTrustNodeNodesOrder, + config.CategoryAnnotation: "Routing", + }, + }) + if err != nil { + return err + } + cfgOptionTrustNodeNodes = config.Concurrent.GetAsStringArray(CfgOptionTrustNodeNodesKey, []string{}) + + err = config.Register(&config.Option{ + Name: "Special Access Code", + Key: cfgOptionSpecialAccessCodeKey, + Description: "Special Access Codes grant access to the SPN for testing or evaluation purposes.", + Sensitive: true, + OptType: config.OptTypeString, + DefaultValue: cfgOptionSpecialAccessCodeDefault, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: cfgOptionSpecialAccessCodeOrder, + config.CategoryAnnotation: "Advanced", + }, + }) + if err != nil { + return err + } + cfgOptionSpecialAccessCode = config.Concurrent.GetAsString(cfgOptionSpecialAccessCodeKey, "") + + if conf.PublicHub() { + err = config.Register(&config.Option{ + Name: "Connect From Advertised IPs Only", + Key: cfgOptionBindToAdvertisedKey, + Description: "Only connect from (bind to) the advertised IP addresses.", + OptType: config.OptTypeBool, + ExpertiseLevel: config.ExpertiseLevelExpert, + DefaultValue: cfgOptionBindToAdvertisedDefault, + RequiresRestart: true, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: cfgOptionBindToAdvertisedOrder, + }, + }) + if err != nil { + return err + } + cfgOptionBindToAdvertised = config.GetAsBool(cfgOptionBindToAdvertisedKey, cfgOptionBindToAdvertisedDefault) + } + + // Config options for use. + cfgOptionRoutingAlgorithm = config.Concurrent.GetAsString(profile.CfgOptionRoutingAlgorithmKey, navigator.DefaultRoutingProfileID) + + return nil +} + +var ( + homeHubPolicy endpoints.Endpoints + homeHubPolicyLock sync.Mutex + homeHubPolicyConfigFlag = config.NewValidityFlag() +) + +func getHomeHubPolicy() (endpoints.Endpoints, error) { + homeHubPolicyLock.Lock() + defer homeHubPolicyLock.Unlock() + + // Return cached value if config is still valid. + if homeHubPolicyConfigFlag.IsValid() { + return homeHubPolicy, nil + } + homeHubPolicyConfigFlag.Refresh() + + // Parse new policy. + policy, err := endpoints.ParseEndpoints(cfgOptionHomeHubPolicy()) + if err != nil { + homeHubPolicy = nil + return nil, err + } + + // Save and return the new policy. + homeHubPolicy = policy + return homeHubPolicy, nil +} + +var ( + dnsExitHubPolicy endpoints.Endpoints + dnsExitHubPolicyLock sync.Mutex + dnsExitHubPolicyConfigFlag = config.NewValidityFlag() +) + +// GetDNSExitHubPolicy return the current DNS exit policy. +func GetDNSExitHubPolicy() (endpoints.Endpoints, error) { + dnsExitHubPolicyLock.Lock() + defer dnsExitHubPolicyLock.Unlock() + + // Return cached value if config is still valid. + if dnsExitHubPolicyConfigFlag.IsValid() { + return dnsExitHubPolicy, nil + } + dnsExitHubPolicyConfigFlag.Refresh() + + // Parse new policy. + policy, err := endpoints.ParseEndpoints(cfgOptionDNSExitHubPolicy()) + if err != nil { + dnsExitHubPolicy = nil + return nil, err + } + + // Save and return the new policy. + dnsExitHubPolicy = policy + return dnsExitHubPolicy, nil +} diff --git a/spn/captain/establish.go b/spn/captain/establish.go new file mode 100644 index 00000000..479098a5 --- /dev/null +++ b/spn/captain/establish.go @@ -0,0 +1,105 @@ +package captain + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/docks" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/ships" + "github.com/safing/portmaster/spn/terminal" +) + +// EstablishCrane establishes a crane to another Hub. +func EstablishCrane(callerCtx context.Context, dst *hub.Hub) (*docks.Crane, error) { + if conf.PublicHub() && dst.ID == publicIdentity.ID { + return nil, errors.New("connecting to self") + } + if docks.GetAssignedCrane(dst.ID) != nil { + return nil, fmt.Errorf("route to %s already exists", dst.ID) + } + + ship, err := ships.Launch(callerCtx, dst, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to launch ship: %w", err) + } + + // On pure clients, mark all ships as public in order to show unmasked data in logs. + if conf.Client() && !conf.PublicHub() { + ship.MarkPublic() + } + + crane, err := docks.NewCrane(ship, dst, publicIdentity) + if err != nil { + return nil, fmt.Errorf("failed to create crane: %w", err) + } + + err = crane.Start(callerCtx) + if err != nil { + return nil, fmt.Errorf("failed to start crane: %w", err) + } + + // Start gossip op for live map updates. + _, tErr := NewGossipOp(crane.Controller) + if tErr != nil { + crane.Stop(tErr) + return nil, fmt.Errorf("failed to start gossip op: %w", tErr) + } + + return crane, nil +} + +// EstablishPublicLane establishes a crane to another Hub and publishes it. +func EstablishPublicLane(ctx context.Context, dst *hub.Hub) (*docks.Crane, *terminal.Error) { + // Create new context with timeout. + // The maximum timeout is a worst case safeguard. + // Keep in mind that multiple IPs and protocols may be tried in all configurations. + // Some servers will be (possibly on purpose) hard to reach. + ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) + defer cancel() + + // Connect to destination and establish communication. + crane, err := EstablishCrane(ctx, dst) + if err != nil { + return nil, terminal.ErrInternalError.With("failed to establish crane: %w", err) + } + + // Publish as Lane. + publishOp, tErr := NewPublishOp(crane.Controller, publicIdentity) + if tErr != nil { + return nil, terminal.ErrInternalError.With("failed to publish: %w", err) + } + + // Wait for publishing to complete. + select { + case tErr := <-publishOp.Result(): + if !tErr.Is(terminal.ErrExplicitAck) { + // Stop crane again, because we failed to publish it. + defer crane.Stop(nil) + return nil, terminal.ErrInternalError.With("failed to publish lane: %w", tErr) + } + + case <-crane.Controller.Ctx().Done(): + defer crane.Stop(nil) + return nil, terminal.ErrStopping + + case <-ctx.Done(): + defer crane.Stop(nil) + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return nil, terminal.ErrTimeout + } + return nil, terminal.ErrCanceled + } + + // Query all gossip msgs. + _, tErr = NewGossipQueryOp(crane.Controller) + if tErr != nil { + log.Warningf("spn/captain: failed to start initial gossip query: %s", tErr) + } + + return crane, nil +} diff --git a/spn/captain/exceptions.go b/spn/captain/exceptions.go new file mode 100644 index 00000000..bde30950 --- /dev/null +++ b/spn/captain/exceptions.go @@ -0,0 +1,28 @@ +package captain + +import ( + "net" + "sync" +) + +var ( + exceptionLock sync.Mutex + exceptIPv4 net.IP + exceptIPv6 net.IP +) + +func setExceptions(ipv4, ipv6 net.IP) { + exceptionLock.Lock() + defer exceptionLock.Unlock() + + exceptIPv4 = ipv4 + exceptIPv6 = ipv6 +} + +// IsExcepted checks if the given IP is currently excepted from the SPN. +func IsExcepted(ip net.IP) bool { + exceptionLock.Lock() + defer exceptionLock.Unlock() + + return ip.Equal(exceptIPv4) || ip.Equal(exceptIPv6) +} diff --git a/spn/captain/gossip.go b/spn/captain/gossip.go new file mode 100644 index 00000000..3279367a --- /dev/null +++ b/spn/captain/gossip.go @@ -0,0 +1,38 @@ +package captain + +import ( + "sync" +) + +var ( + gossipOps = make(map[string]*GossipOp) + gossipOpsLock sync.RWMutex +) + +func registerGossipOp(craneID string, op *GossipOp) { + gossipOpsLock.Lock() + defer gossipOpsLock.Unlock() + + gossipOps[craneID] = op +} + +func deleteGossipOp(craneID string) { + gossipOpsLock.Lock() + defer gossipOpsLock.Unlock() + + delete(gossipOps, craneID) +} + +func gossipRelayMsg(receivedFrom string, msgType GossipMsgType, data []byte) { + gossipOpsLock.RLock() + defer gossipOpsLock.RUnlock() + + for craneID, gossipOp := range gossipOps { + // Don't return same msg back to sender. + if craneID == receivedFrom { + continue + } + + gossipOp.sendMsg(msgType, data) + } +} diff --git a/spn/captain/hooks.go b/spn/captain/hooks.go new file mode 100644 index 00000000..6a60f7ea --- /dev/null +++ b/spn/captain/hooks.go @@ -0,0 +1,47 @@ +package captain + +import ( + "time" + + "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/docks" +) + +func startDockHooks() { + docks.RegisterCraneUpdateHook(handleCraneUpdate) +} + +func stopDockHooks() { + docks.ResetCraneUpdateHook() +} + +func handleCraneUpdate(crane *docks.Crane) { + if crane == nil { + return + } + + if conf.Client() && crane.Controller != nil && crane.Controller.Abandoning.IsSet() { + // Check connection to home hub. + triggerClientHealthCheck() + } + + if conf.PublicHub() && crane.Public() { + // Update Hub status. + updateConnectionStatus() + } +} + +func updateConnectionStatus() { + // Delay updating status for a better chance to combine multiple changes. + statusUpdateTask.Schedule(time.Now().Add(maintainStatusUpdateDelay)) + + // Check if we lost all connections and trigger a pending restart if we did. + for _, crane := range docks.GetAllAssignedCranes() { + if crane.Public() && !crane.Stopped() { + // There is at least one public and active crane, so don't restart now. + return + } + } + updates.TriggerRestartIfPending() +} diff --git a/spn/captain/intel.go b/spn/captain/intel.go new file mode 100644 index 00000000..fe743c1b --- /dev/null +++ b/spn/captain/intel.go @@ -0,0 +1,108 @@ +package captain + +import ( + "context" + "fmt" + "os" + "sync" + + "github.com/safing/portbase/config" + "github.com/safing/portbase/updater" + "github.com/safing/portmaster/service/updates" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/navigator" + "github.com/safing/portmaster/spn/ships" +) + +var ( + intelResource *updater.File + intelResourcePath = "intel/spn/main-intel.yaml" + intelResourceMapName = "main" + intelResourceUpdateLock sync.Mutex +) + +func registerIntelUpdateHook() error { + if err := module.RegisterEventHook( + updates.ModuleName, + updates.ResourceUpdateEvent, + "update SPN intel", + updateSPNIntel, + ); err != nil { + return err + } + + if err := module.RegisterEventHook( + "config", + config.ChangeEvent, + "update SPN intel", + updateSPNIntel, + ); err != nil { + return err + } + + return nil +} + +func updateSPNIntel(ctx context.Context, _ interface{}) (err error) { + intelResourceUpdateLock.Lock() + defer intelResourceUpdateLock.Unlock() + + // Only update SPN intel when using the matching map. + if conf.MainMapName != intelResourceMapName { + return fmt.Errorf("intel resource not for map %q", conf.MainMapName) + } + + // Check if there is something to do. + if intelResource != nil && !intelResource.UpgradeAvailable() { + return nil + } + + // Get intel file and load it from disk. + intelResource, err = updates.GetFile(intelResourcePath) + if err != nil { + return fmt.Errorf("failed to get SPN intel update: %w", err) + } + intelData, err := os.ReadFile(intelResource.Path()) + if err != nil { + return fmt.Errorf("failed to load SPN intel update: %w", err) + } + + // Parse and apply intel data. + intel, err := hub.ParseIntel(intelData) + if err != nil { + return fmt.Errorf("failed to parse SPN intel update: %w", err) + } + + setVirtualNetworkConfig(intel.VirtualNetworks) + return navigator.Main.UpdateIntel(intel, cfgOptionTrustNodeNodes()) +} + +func resetSPNIntel() { + intelResourceUpdateLock.Lock() + defer intelResourceUpdateLock.Unlock() + + intelResource = nil +} + +func setVirtualNetworkConfig(configs []*hub.VirtualNetworkConfig) { + // Do nothing if not public Hub. + if !conf.PublicHub() { + return + } + // Reset if there are no virtual networks configured. + if len(configs) == 0 { + ships.SetVirtualNetworkConfig(nil) + } + + // Check if we are in a virtual network. + for _, config := range configs { + if _, ok := config.Mapping[publicIdentity.Hub.ID]; ok { + ships.SetVirtualNetworkConfig(config) + return + } + } + + // If not, reset - we might have been in one before. + ships.SetVirtualNetworkConfig(nil) +} diff --git a/spn/captain/module.go b/spn/captain/module.go new file mode 100644 index 00000000..356eb199 --- /dev/null +++ b/spn/captain/module.go @@ -0,0 +1,219 @@ +package captain + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "time" + + "github.com/safing/portbase/api" + "github.com/safing/portbase/config" + "github.com/safing/portbase/log" + "github.com/safing/portbase/modules" + "github.com/safing/portbase/modules/subsystems" + "github.com/safing/portbase/rng" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/crew" + "github.com/safing/portmaster/spn/navigator" + "github.com/safing/portmaster/spn/patrol" + "github.com/safing/portmaster/spn/ships" + _ "github.com/safing/portmaster/spn/sluice" +) + +const controlledFailureExitCode = 24 + +var module *modules.Module + +// SPNConnectedEvent is the name of the event that is fired when the SPN has connected and is ready. +const SPNConnectedEvent = "spn connect" + +func init() { + module = modules.Register("captain", prep, start, stop, "base", "terminal", "cabin", "ships", "docks", "crew", "navigator", "sluice", "patrol", "netenv") + module.RegisterEvent(SPNConnectedEvent, false) + subsystems.Register( + "spn", + "SPN", + "Safing Privacy Network", + module, + "config:spn/", + &config.Option{ + Name: "SPN Module", + Key: CfgOptionEnableSPNKey, + Description: "Start the Safing Privacy Network module. If turned off, the SPN is fully disabled on this device.", + OptType: config.OptTypeBool, + DefaultValue: false, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: cfgOptionEnableSPNOrder, + config.CategoryAnnotation: "General", + }, + }, + ) +} + +func prep() error { + // Check if we can parse the bootstrap hub flag. + if err := prepBootstrapHubFlag(); err != nil { + return err + } + + // Register SPN status provider. + if err := registerSPNStatusProvider(); err != nil { + return err + } + + // Register API endpoints. + if err := registerAPIEndpoints(); err != nil { + return err + } + + if conf.PublicHub() { + // Register API authenticator. + if err := api.SetAuthenticator(apiAuthenticator); err != nil { + return err + } + + if err := module.RegisterEventHook( + "patrol", + patrol.ChangeSignalEventName, + "trigger hub status maintenance", + func(_ context.Context, _ any) error { + TriggerHubStatusMaintenance() + return nil + }, + ); err != nil { + return err + } + } + + return prepConfig() +} + +func start() error { + maskingBytes, err := rng.Bytes(16) + if err != nil { + return fmt.Errorf("failed to get random bytes for masking: %w", err) + } + ships.EnableMasking(maskingBytes) + + // Initialize intel. + if err := registerIntelUpdateHook(); err != nil { + return err + } + if err := updateSPNIntel(module.Ctx, nil); err != nil { + log.Errorf("spn/captain: failed to update SPN intel: %s", err) + } + + // Initialize identity and piers. + if conf.PublicHub() { + // Load identity. + if err := loadPublicIdentity(); err != nil { + // We cannot recover from this, set controlled failure (do not retry). + modules.SetExitStatusCode(controlledFailureExitCode) + + return err + } + + // Check if any networks are configured. + if !conf.HubHasIPv4() && !conf.HubHasIPv6() { + // We cannot recover from this, set controlled failure (do not retry). + modules.SetExitStatusCode(controlledFailureExitCode) + + return errors.New("no IP addresses for Hub configured (or detected)") + } + + // Start management of identity and piers. + if err := prepPublicIdentityMgmt(); err != nil { + return err + } + // Set ID to display on http info page. + ships.DisplayHubID = publicIdentity.ID + // Start listeners. + if err := startPiers(); err != nil { + return err + } + + // Enable connect operation. + crew.EnableConnecting(publicIdentity.Hub) + } + + // Subscribe to updates of cranes. + startDockHooks() + + // bootstrapping + if err := processBootstrapHubFlag(); err != nil { + return err + } + if err := processBootstrapFileFlag(); err != nil { + return err + } + + // network optimizer + if conf.PublicHub() { + module.NewTask("optimize network", optimizeNetwork). + Repeat(1 * time.Minute). + Schedule(time.Now().Add(15 * time.Second)) + } + + // client + home hub manager + if conf.Client() { + module.StartServiceWorker("client manager", 0, clientManager) + + // Reset failing hubs when the network changes while not connected. + if err := module.RegisterEventHook( + "netenv", + "network changed", + "reset failing hubs", + func(_ context.Context, _ interface{}) error { + if ready.IsNotSet() { + navigator.Main.ResetFailingStates(module.Ctx) + } + return nil + }, + ); err != nil { + return err + } + } + + return nil +} + +func stop() error { + // Reset intel resource so that it is loaded again when starting. + resetSPNIntel() + + // Unregister crane update hook. + stopDockHooks() + + // Send shutdown status message. + if conf.PublicHub() { + publishShutdownStatus() + stopPiers() + } + + return nil +} + +// apiAuthenticator grants User permissions for local API requests. +func apiAuthenticator(r *http.Request, s *http.Server) (*api.AuthToken, error) { + // Get remote IP. + host, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return nil, fmt.Errorf("failed to split host/port: %w", err) + } + remoteIP := net.ParseIP(host) + if remoteIP == nil { + return nil, fmt.Errorf("failed to parse remote address %s", host) + } + + if !netutils.GetIPScope(remoteIP).IsLocalhost() { + return nil, api.ErrAPIAccessDeniedMessage + } + + return &api.AuthToken{ + Read: api.PermitUser, + Write: api.PermitUser, + }, nil +} diff --git a/spn/captain/navigation.go b/spn/captain/navigation.go new file mode 100644 index 00000000..e60267fa --- /dev/null +++ b/spn/captain/navigation.go @@ -0,0 +1,306 @@ +package captain + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/safing/portbase/log" + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/profile/endpoints" + "github.com/safing/portmaster/spn/access" + "github.com/safing/portmaster/spn/docks" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/navigator" + "github.com/safing/portmaster/spn/terminal" +) + +const stopCraneAfterBeingUnsuggestedFor = 6 * time.Hour + +var ( + // ErrAllHomeHubsExcluded is returned when all available home hubs were excluded. + ErrAllHomeHubsExcluded = errors.New("all home hubs are excluded") + + // ErrReInitSPNSuggested is returned when no home hub can be found, even without rules. + ErrReInitSPNSuggested = errors.New("SPN re-init suggested") +) + +func establishHomeHub(ctx context.Context) error { + // Get own IP. + locations, ok := netenv.GetInternetLocation() + if !ok || len(locations.All) == 0 { + return errors.New("failed to locate own device") + } + log.Debugf( + "spn/captain: looking for new home hub near %s and %s", + locations.BestV4(), + locations.BestV6(), + ) + + // Get own entity. + // Checking the entity against the entry policies is somewhat hit and miss + // anyway, as the device location is an approximation. + var myEntity *intel.Entity + if dl := locations.BestV4(); dl != nil && dl.IP != nil { + myEntity = (&intel.Entity{IP: dl.IP}).Init(0) + myEntity.FetchData(ctx) + } else if dl := locations.BestV6(); dl != nil && dl.IP != nil { + myEntity = (&intel.Entity{IP: dl.IP}).Init(0) + myEntity.FetchData(ctx) + } + + // Get home hub policy for selecting the home hub. + homePolicy, err := getHomeHubPolicy() + if err != nil { + return err + } + + // Build navigation options for searching for a home hub. + opts := &navigator.Options{ + Home: &navigator.HomeHubOptions{ + HubPolicies: []endpoints.Endpoints{homePolicy}, + CheckHubPolicyWith: myEntity, + }, + } + + // Add requirement to only use Safing nodes when not using community nodes. + if !cfgOptionUseCommunityNodes() { + opts.Home.RequireVerifiedOwners = NonCommunityVerifiedOwners + } + + // Require a trusted home node when the routing profile requires less than two hops. + routingProfile := navigator.GetRoutingProfile(cfgOptionRoutingAlgorithm()) + if routingProfile.MinHops < 2 { + opts.Home.Regard = opts.Home.Regard.Add(navigator.StateTrusted) + } + + // Find nearby hubs. +findCandidates: + candidates, err := navigator.Main.FindNearestHubs( + locations.BestV4().LocationOrNil(), + locations.BestV6().LocationOrNil(), + opts, navigator.HomeHub, + ) + if err != nil { + switch { + case errors.Is(err, navigator.ErrEmptyMap): + // bootstrap to the network! + err := bootstrapWithUpdates() + if err != nil { + return err + } + goto findCandidates + + case errors.Is(err, navigator.ErrAllPinsDisregarded): + if len(homePolicy) > 0 { + return ErrAllHomeHubsExcluded + } + return ErrReInitSPNSuggested + + default: + return fmt.Errorf("failed to find nearby hubs: %w", err) + } + } + + // Try connecting to a hub. + var tries int + var candidate *hub.Hub + for tries, candidate = range candidates { + err = connectToHomeHub(ctx, candidate) + if err != nil { + // Check if context is canceled. + if ctx.Err() != nil { + return ctx.Err() + } + // Check if the SPN protocol is stopping again. + if errors.Is(err, terminal.ErrStopping) { + return err + } + log.Warningf("spn/captain: failed to connect to %s as new home: %s", candidate, err) + } else { + log.Infof("spn/captain: established connection to %s as new home with %d failed tries", candidate, tries) + return nil + } + } + if err != nil { + return fmt.Errorf("failed to connect to a new home hub - tried %d hubs: %w", tries+1, err) + } + return fmt.Errorf("no home hub candidates available") +} + +func connectToHomeHub(ctx context.Context, dst *hub.Hub) error { + // Create new context with timeout. + // The maximum timeout is a worst case safeguard. + // Keep in mind that multiple IPs and protocols may be tried in all configurations. + // Some servers will be (possibly on purpose) hard to reach. + ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) + defer cancel() + + // Set and clean up exceptions. + setExceptions(dst.Info.IPv4, dst.Info.IPv6) + defer setExceptions(nil, nil) + + // Connect to hub. + crane, err := EstablishCrane(ctx, dst) + if err != nil { + return err + } + + // Cleanup connection in case of failure. + var success bool + defer func() { + if !success { + crane.Stop(nil) + } + }() + + // Query all gossip msgs on first connection. + gossipQuery, tErr := NewGossipQueryOp(crane.Controller) + if tErr != nil { + log.Warningf("spn/captain: failed to start initial gossip query: %s", tErr) + } + // Wait for gossip query to complete. + select { + case <-gossipQuery.ctx.Done(): + case <-ctx.Done(): + return context.Canceled + } + + // Create communication terminal. + homeTerminal, initData, tErr := docks.NewLocalCraneTerminal(crane, nil, terminal.DefaultHomeHubTerminalOpts()) + if tErr != nil { + return tErr.Wrap("failed to create home terminal") + } + tErr = crane.EstablishNewTerminal(homeTerminal, initData) + if tErr != nil { + return tErr.Wrap("failed to connect home terminal") + } + + if !DisableAccount { + // Authenticate to home hub. + authOp, tErr := access.AuthorizeToTerminal(homeTerminal) + if tErr != nil { + return tErr.Wrap("failed to authorize") + } + select { + case tErr := <-authOp.Result: + if !tErr.Is(terminal.ErrExplicitAck) { + return tErr.Wrap("failed to authenticate to") + } + case <-time.After(3 * time.Second): + return terminal.ErrTimeout.With("waiting for auth to complete") + case <-ctx.Done(): + return terminal.ErrStopping + } + } + + // Set new home on map. + ok := navigator.Main.SetHome(dst.ID, homeTerminal) + if !ok { + return fmt.Errorf("failed to set home hub on map") + } + + // Assign crane to home hub in order to query it later. + docks.AssignCrane(crane.ConnectedHub.ID, crane) + + success = true + return nil +} + +func optimizeNetwork(ctx context.Context, task *modules.Task) error { + if publicIdentity == nil { + return nil + } + +optimize: + result, err := navigator.Main.Optimize(nil) + if err != nil { + if errors.Is(err, navigator.ErrEmptyMap) { + // bootstrap to the network! + err := bootstrapWithUpdates() + if err != nil { + return err + } + goto optimize + } + + return err + } + + // Create any new connections. + var createdConnections int + var attemptedConnections int + for _, connectTo := range result.SuggestedConnections { + // Skip duplicates. + if connectTo.Duplicate { + continue + } + + // Check if connection already exists. + crane := docks.GetAssignedCrane(connectTo.Hub.ID) + if crane != nil { + // Update last suggested timestamp. + crane.NetState.UpdateLastSuggestedAt() + // Continue crane if stopping. + if crane.AbortStopping() { + log.Infof("spn/captain: optimization aborted retiring of %s, removed stopping mark", crane) + crane.NotifyUpdate() + } + + // Create new connections if we have connects left. + } else if createdConnections < result.MaxConnect { + attemptedConnections++ + + crane, tErr := EstablishPublicLane(ctx, connectTo.Hub) + if !tErr.IsOK() { + log.Warningf("spn/captain: failed to establish lane to %s: %s", connectTo.Hub, tErr) + } else { + createdConnections++ + crane.NetState.UpdateLastSuggestedAt() + + log.Infof("spn/captain: established lane to %s", connectTo.Hub) + } + } + } + + // Log optimization result. + if attemptedConnections > 0 { + log.Infof( + "spn/captain: created %d/%d new connections for %s optimization", + createdConnections, + attemptedConnections, + result.Purpose) + } else { + log.Infof( + "spn/captain: checked %d connections for %s optimization", + len(result.SuggestedConnections), + result.Purpose, + ) + } + + // Retire cranes if unsuggested for a while. + if result.StopOthers { + for _, crane := range docks.GetAllAssignedCranes() { + switch { + case crane.Stopped(): + // Crane already stopped. + case crane.IsStopping(): + // Crane is stopping, forcibly stop if mine and suggested. + if crane.IsMine() && crane.NetState.StopSuggested() { + crane.Stop(nil) + } + case crane.IsMine() && crane.NetState.StoppingSuggested(): + // Mark as stopping if mine and suggested. + crane.MarkStopping() + case crane.NetState.RequestStoppingSuggested(stopCraneAfterBeingUnsuggestedFor): + // Mark as stopping requested. + crane.MarkStoppingRequested() + } + } + } + + return nil +} diff --git a/spn/captain/op_gossip.go b/spn/captain/op_gossip.go new file mode 100644 index 00000000..e5fb4377 --- /dev/null +++ b/spn/captain/op_gossip.go @@ -0,0 +1,156 @@ +package captain + +import ( + "time" + + "github.com/safing/portbase/container" + "github.com/safing/portbase/formats/varint" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/docks" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/terminal" +) + +// GossipOpType is the type ID of the gossip operation. +const GossipOpType string = "gossip" + +// GossipMsgType is the gossip message type. +type GossipMsgType uint8 + +// Gossip Message Types. +const ( + GossipHubAnnouncementMsg GossipMsgType = 1 + GossipHubStatusMsg GossipMsgType = 2 +) + +func (msgType GossipMsgType) String() string { + switch msgType { + case GossipHubAnnouncementMsg: + return "hub announcement" + case GossipHubStatusMsg: + return "hub status" + default: + return "unknown gossip msg" + } +} + +// GossipOp is used to gossip Hub messages. +type GossipOp struct { + terminal.OperationBase + + craneID string +} + +// Type returns the type ID. +func (op *GossipOp) Type() string { + return GossipOpType +} + +func init() { + terminal.RegisterOpType(terminal.OperationFactory{ + Type: GossipOpType, + Requires: terminal.IsCraneController, + Start: runGossipOp, + }) +} + +// NewGossipOp start a new gossip operation. +func NewGossipOp(controller *docks.CraneControllerTerminal) (*GossipOp, *terminal.Error) { + // Create and init. + op := &GossipOp{ + craneID: controller.Crane.ID, + } + err := controller.StartOperation(op, nil, 1*time.Minute) + if err != nil { + return nil, err + } + op.InitOperationBase(controller, op.ID()) + + // Register and return. + registerGossipOp(controller.Crane.ID, op) + return op, nil +} + +func runGossipOp(t terminal.Terminal, opID uint32, data *container.Container) (terminal.Operation, *terminal.Error) { + // Check if we are run by a controller. + controller, ok := t.(*docks.CraneControllerTerminal) + if !ok { + return nil, terminal.ErrIncorrectUsage.With("gossip op may only be started by a crane controller terminal, but was started by %T", t) + } + + // Create, init, register and return. + op := &GossipOp{ + craneID: controller.Crane.ID, + } + op.InitOperationBase(t, opID) + registerGossipOp(controller.Crane.ID, op) + return op, nil +} + +func (op *GossipOp) sendMsg(msgType GossipMsgType, data []byte) { + // Create message. + msg := op.NewEmptyMsg() + msg.Data = container.New( + varint.Pack8(uint8(msgType)), + data, + ) + msg.Unit.MakeHighPriority() + + // Send. + err := op.Send(msg, 1*time.Second) + if err != nil { + log.Debugf("spn/captain: failed to forward %s via %s: %s", msgType, op.craneID, err) + } +} + +// Deliver delivers a message to the operation. +func (op *GossipOp) Deliver(msg *terminal.Msg) *terminal.Error { + defer msg.Finish() + + gossipMsgTypeN, err := msg.Data.GetNextN8() + if err != nil { + return terminal.ErrMalformedData.With("failed to parse gossip message type") + } + gossipMsgType := GossipMsgType(gossipMsgTypeN) + + // Prepare data. + data := msg.Data.CompileData() + var announcementData, statusData []byte + switch gossipMsgType { + case GossipHubAnnouncementMsg: + announcementData = data + case GossipHubStatusMsg: + statusData = data + default: + log.Warningf("spn/captain: received unknown gossip message type from %s: %d", op.craneID, gossipMsgType) + return nil + } + + // Import and verify. + h, forward, tErr := docks.ImportAndVerifyHubInfo(module.Ctx, "", announcementData, statusData, conf.MainMapName, conf.MainMapScope) + if tErr != nil { + if tErr.Is(hub.ErrOldData) { + log.Debugf("spn/captain: ignoring old %s from %s", gossipMsgType, op.craneID) + } else { + log.Warningf("spn/captain: failed to import %s from %s: %s", gossipMsgType, op.craneID, tErr) + } + } else if forward { + // Only log if we received something to save/forward. + log.Infof("spn/captain: received %s for %s", gossipMsgType, h) + } + + // Relay data. + if forward { + gossipRelayMsg(op.craneID, gossipMsgType, data) + } + return nil +} + +// HandleStop gives the operation the ability to cleanly shut down. +// The returned error is the error to send to the other side. +// Should never be called directly. Call Stop() instead. +func (op *GossipOp) HandleStop(err *terminal.Error) (errorToSend *terminal.Error) { + deleteGossipOp(op.craneID) + return err +} diff --git a/spn/captain/op_gossip_query.go b/spn/captain/op_gossip_query.go new file mode 100644 index 00000000..aaadbc21 --- /dev/null +++ b/spn/captain/op_gossip_query.go @@ -0,0 +1,195 @@ +package captain + +import ( + "context" + "strings" + "time" + + "github.com/safing/portbase/container" + "github.com/safing/portbase/formats/varint" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/docks" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/terminal" +) + +// GossipQueryOpType is the type ID of the gossip query operation. +const GossipQueryOpType string = "gossip/query" + +// GossipQueryOp is used to query gossip messages. +type GossipQueryOp struct { + terminal.OperationBase + + t terminal.Terminal + client bool + importCnt int + + ctx context.Context + cancelCtx context.CancelFunc +} + +// Type returns the type ID. +func (op *GossipQueryOp) Type() string { + return GossipQueryOpType +} + +func init() { + terminal.RegisterOpType(terminal.OperationFactory{ + Type: GossipQueryOpType, + Requires: terminal.IsCraneController, + Start: runGossipQueryOp, + }) +} + +// NewGossipQueryOp starts a new gossip query operation. +func NewGossipQueryOp(t terminal.Terminal) (*GossipQueryOp, *terminal.Error) { + // Create and init. + op := &GossipQueryOp{ + t: t, + client: true, + } + op.ctx, op.cancelCtx = context.WithCancel(t.Ctx()) + err := t.StartOperation(op, nil, 1*time.Minute) + if err != nil { + return nil, err + } + return op, nil +} + +func runGossipQueryOp(t terminal.Terminal, opID uint32, data *container.Container) (terminal.Operation, *terminal.Error) { + // Create, init, register and return. + op := &GossipQueryOp{t: t} + op.ctx, op.cancelCtx = context.WithCancel(t.Ctx()) + op.InitOperationBase(t, opID) + + module.StartWorker("gossip query handler", op.handler) + + return op, nil +} + +func (op *GossipQueryOp) handler(_ context.Context) error { + tErr := op.sendMsgs(hub.MsgTypeAnnouncement) + if tErr != nil { + op.Stop(op, tErr) + return nil // Clean worker exit. + } + + tErr = op.sendMsgs(hub.MsgTypeStatus) + if tErr != nil { + op.Stop(op, tErr) + return nil // Clean worker exit. + } + + op.Stop(op, nil) + return nil // Clean worker exit. +} + +func (op *GossipQueryOp) sendMsgs(msgType hub.MsgType) *terminal.Error { + it, err := hub.QueryRawGossipMsgs(conf.MainMapName, msgType) + if err != nil { + return terminal.ErrInternalError.With("failed to query: %w", err) + } + defer it.Cancel() + +iterating: + for { + select { + case r := <-it.Next: + // Check if we are done. + if r == nil { + return nil + } + + // Ensure we're handling a hub msg. + hubMsg, err := hub.EnsureHubMsg(r) + if err != nil { + log.Warningf("spn/captain: failed to load hub msg: %s", err) + continue iterating + } + + // Create gossip msg. + var c *container.Container + switch hubMsg.Type { + case hub.MsgTypeAnnouncement: + c = container.New( + varint.Pack8(uint8(GossipHubAnnouncementMsg)), + hubMsg.Data, + ) + case hub.MsgTypeStatus: + c = container.New( + varint.Pack8(uint8(GossipHubStatusMsg)), + hubMsg.Data, + ) + default: + log.Warningf("spn/captain: unknown hub msg for gossip query at %q: %s", hubMsg.Key(), hubMsg.Type) + } + + // Send msg. + if c != nil { + msg := op.NewEmptyMsg() + msg.Unit.MakeHighPriority() + msg.Data = c + tErr := op.Send(msg, 1*time.Second) + if tErr != nil { + return tErr.Wrap("failed to send msg") + } + } + + case <-op.ctx.Done(): + return terminal.ErrStopping + } + } +} + +// Deliver delivers the message to the operation. +func (op *GossipQueryOp) Deliver(msg *terminal.Msg) *terminal.Error { + defer msg.Finish() + + gossipMsgTypeN, err := msg.Data.GetNextN8() + if err != nil { + return terminal.ErrMalformedData.With("failed to parse gossip message type") + } + gossipMsgType := GossipMsgType(gossipMsgTypeN) + + // Prepare data. + data := msg.Data.CompileData() + var announcementData, statusData []byte + switch gossipMsgType { + case GossipHubAnnouncementMsg: + announcementData = data + case GossipHubStatusMsg: + statusData = data + default: + log.Warningf("spn/captain: received unknown gossip message type from gossip query: %d", gossipMsgType) + return nil + } + + // Import and verify. + h, forward, tErr := docks.ImportAndVerifyHubInfo(module.Ctx, "", announcementData, statusData, conf.MainMapName, conf.MainMapScope) + if tErr != nil { + log.Warningf("spn/captain: failed to import %s from gossip query: %s", gossipMsgType, tErr) + } else { + log.Infof("spn/captain: received %s for %s from gossip query", gossipMsgType, h) + op.importCnt++ + } + + // Relay data. + if forward { + // TODO: Find better way to get craneID. + craneID := strings.SplitN(op.t.FmtID(), "#", 2)[0] + gossipRelayMsg(craneID, gossipMsgType, data) + } + return nil +} + +// HandleStop gives the operation the ability to cleanly shut down. +// The returned error is the error to send to the other side. +// Should never be called directly. Call Stop() instead. +func (op *GossipQueryOp) HandleStop(err *terminal.Error) (errorToSend *terminal.Error) { + if op.client { + log.Infof("spn/captain: gossip query imported %d entries", op.importCnt) + } + op.cancelCtx() + return err +} diff --git a/spn/captain/op_publish.go b/spn/captain/op_publish.go new file mode 100644 index 00000000..178d1e88 --- /dev/null +++ b/spn/captain/op_publish.go @@ -0,0 +1,183 @@ +package captain + +import ( + "time" + + "github.com/safing/portbase/container" + "github.com/safing/portmaster/spn/cabin" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/docks" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/terminal" +) + +// PublishOpType is the type ID of the publish operation. +const PublishOpType string = "publish" + +// PublishOp is used to publish a connection. +type PublishOp struct { + terminal.OperationBase + controller *docks.CraneControllerTerminal + + identity *cabin.Identity + requestingHub *hub.Hub + verification *cabin.Verification + result chan *terminal.Error +} + +// Type returns the type ID. +func (op *PublishOp) Type() string { + return PublishOpType +} + +func init() { + terminal.RegisterOpType(terminal.OperationFactory{ + Type: PublishOpType, + Requires: terminal.IsCraneController, + Start: runPublishOp, + }) +} + +// NewPublishOp start a new publish operation. +func NewPublishOp(controller *docks.CraneControllerTerminal, identity *cabin.Identity) (*PublishOp, *terminal.Error) { + // Create and init. + op := &PublishOp{ + controller: controller, + identity: identity, + result: make(chan *terminal.Error, 1), + } + msg := container.New() + + // Add Hub Announcement. + announcementData, err := identity.ExportAnnouncement() + if err != nil { + return nil, terminal.ErrInternalError.With("failed to export announcement: %w", err) + } + msg.AppendAsBlock(announcementData) + + // Add Hub Status. + statusData, err := identity.ExportStatus() + if err != nil { + return nil, terminal.ErrInternalError.With("failed to export status: %w", err) + } + msg.AppendAsBlock(statusData) + + tErr := controller.StartOperation(op, msg, 10*time.Second) + if tErr != nil { + return nil, tErr + } + return op, nil +} + +func runPublishOp(t terminal.Terminal, opID uint32, data *container.Container) (terminal.Operation, *terminal.Error) { + // Check if we are run by a controller. + controller, ok := t.(*docks.CraneControllerTerminal) + if !ok { + return nil, terminal.ErrIncorrectUsage.With("publish op may only be started by a crane controller terminal, but was started by %T", t) + } + + // Parse and import Announcement and Status. + announcementData, err := data.GetNextBlock() + if err != nil { + return nil, terminal.ErrMalformedData.With("failed to get announcement: %w", err) + } + statusData, err := data.GetNextBlock() + if err != nil { + return nil, terminal.ErrMalformedData.With("failed to get status: %w", err) + } + h, forward, tErr := docks.ImportAndVerifyHubInfo(module.Ctx, "", announcementData, statusData, conf.MainMapName, conf.MainMapScope) + if tErr != nil { + return nil, tErr.Wrap("failed to import and verify hub") + } + // Update reference in case it was changed by the import. + controller.Crane.ConnectedHub = h + + // Relay data. + if forward { + gossipRelayMsg(controller.Crane.ID, GossipHubAnnouncementMsg, announcementData) + gossipRelayMsg(controller.Crane.ID, GossipHubStatusMsg, statusData) + } + + // Create verification request. + v, request, err := cabin.CreateVerificationRequest(PublishOpType, "", "") + if err != nil { + return nil, terminal.ErrInternalError.With("failed to create verification request: %w", err) + } + + // Create operation. + op := &PublishOp{ + controller: controller, + requestingHub: h, + verification: v, + result: make(chan *terminal.Error, 1), + } + op.InitOperationBase(controller, opID) + + // Reply with verification request. + tErr = op.Send(op.NewMsg(request), 10*time.Second) + if tErr != nil { + return nil, tErr.Wrap("failed to send verification request") + } + + return op, nil +} + +// Deliver delivers a message to the operation. +func (op *PublishOp) Deliver(msg *terminal.Msg) *terminal.Error { + defer msg.Finish() + + if op.identity != nil { + // Client + + // Sign the received verification request. + response, err := op.identity.SignVerificationRequest(msg.Data.CompileData(), PublishOpType, "", "") + if err != nil { + return terminal.ErrPermissionDenied.With("signing verification request failed: %w", err) + } + + return op.Send(op.NewMsg(response), 10*time.Second) + } else if op.requestingHub != nil { + // Server + + // Verify the signed request. + err := op.verification.Verify(msg.Data.CompileData(), op.requestingHub) + if err != nil { + return terminal.ErrPermissionDenied.With("checking verification request failed: %w", err) + } + return terminal.ErrExplicitAck + } + + return terminal.ErrInternalError.With("invalid operation state") +} + +// Result returns the result (end error) of the operation. +func (op *PublishOp) Result() <-chan *terminal.Error { + return op.result +} + +// HandleStop gives the operation the ability to cleanly shut down. +// The returned error is the error to send to the other side. +// Should never be called directly. Call Stop() instead. +func (op *PublishOp) HandleStop(tErr *terminal.Error) (errorToSend *terminal.Error) { + if tErr.Is(terminal.ErrExplicitAck) { + // TODO: Check for concurrenct access. + if op.controller.Crane.ConnectedHub == nil { + op.controller.Crane.ConnectedHub = op.requestingHub + } + + // Publish crane, abort if it fails. + err := op.controller.Crane.Publish() + if err != nil { + tErr = terminal.ErrInternalError.With("failed to publish crane: %w", err) + op.controller.Crane.Stop(tErr) + } else { + op.controller.Crane.NotifyUpdate() + } + } + + select { + case op.result <- tErr: + default: + } + return tErr +} diff --git a/spn/captain/piers.go b/spn/captain/piers.go new file mode 100644 index 00000000..b0c994bf --- /dev/null +++ b/spn/captain/piers.go @@ -0,0 +1,131 @@ +package captain + +import ( + "context" + "errors" + "fmt" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/service/profile/endpoints" + "github.com/safing/portmaster/spn/docks" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/ships" +) + +var ( + dockingRequests = make(chan ships.Ship, 100) + piers []ships.Pier +) + +func startPiers() error { + // Get and check transports. + transports := publicIdentity.Hub.Info.Transports + if len(transports) == 0 { + return errors.New("no transports defined") + } + + piers = make([]ships.Pier, 0, len(transports)) + for _, t := range transports { + // Parse transport. + transport, err := hub.ParseTransport(t) + if err != nil { + return fmt.Errorf("cannot build pier for invalid transport %q: %w", t, err) + } + + // Establish pier / listener. + pier, err := ships.EstablishPier(transport, dockingRequests) + if err != nil { + return fmt.Errorf("failed to establish pier for transport %q: %w", t, err) + } + + piers = append(piers, pier) + log.Infof("spn/captain: pier for transport %q built", t) + } + + // Start worker to handle docking requests. + module.StartServiceWorker("docking request handler", 0, dockingRequestHandler) + + return nil +} + +func stopPiers() { + for _, pier := range piers { + pier.Abolish() + } +} + +func dockingRequestHandler(ctx context.Context) error { + // Sink all waiting ships when this worker ends. + // But don't be destructive so the service worker could recover. + defer func() { + for { + select { + case ship := <-dockingRequests: + if ship != nil { + ship.Sink() + } + default: + return + } + } + }() + + for { + select { + case <-ctx.Done(): + return nil + case ship := <-dockingRequests: + // Ignore nil ships. + if ship == nil { + continue + } + + if err := checkDockingPermission(ctx, ship); err != nil { + log.Warningf("spn/captain: denied ship from %s to dock at pier %s: %s", ship.RemoteAddr(), ship.Transport().String(), err) + } else { + handleDockingRequest(ship) + } + } + } +} + +func checkDockingPermission(ctx context.Context, ship ships.Ship) error { + remoteIP, remotePort, err := netutils.IPPortFromAddr(ship.RemoteAddr()) + if err != nil { + return fmt.Errorf("failed to parse remote IP: %w", err) + } + + // Create entity. + entity := (&intel.Entity{ + IP: remoteIP, + Protocol: uint8(netutils.ProtocolFromNetwork(ship.RemoteAddr().Network())), + Port: remotePort, + }).Init(ship.Transport().Port) + entity.FetchData(ctx) + + // Check against policy. + result, reason := publicIdentity.Hub.GetInfo().EntryPolicy().Match(ctx, entity) + if result == endpoints.Denied { + return fmt.Errorf("entry policy violated: %s", reason) + } + + return nil +} + +func handleDockingRequest(ship ships.Ship) { + log.Infof("spn/captain: pemitting %s to dock", ship) + + crane, err := docks.NewCrane(ship, nil, publicIdentity) + if err != nil { + log.Warningf("spn/captain: failed to commission crane for %s: %s", ship, err) + return + } + + module.StartWorker("start crane", func(ctx context.Context) error { + _ = crane.Start(ctx) + // Crane handles errors internally. + return nil + }) +} diff --git a/spn/captain/public.go b/spn/captain/public.go new file mode 100644 index 00000000..04710d9f --- /dev/null +++ b/spn/captain/public.go @@ -0,0 +1,247 @@ +package captain + +import ( + "context" + "errors" + "fmt" + "sort" + "time" + + "github.com/safing/portbase/database" + "github.com/safing/portbase/log" + "github.com/safing/portbase/metrics" + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/spn/cabin" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/docks" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/navigator" + "github.com/safing/portmaster/spn/patrol" +) + +const ( + maintainStatusInterval = 15 * time.Minute + maintainStatusUpdateDelay = 5 * time.Second +) + +var ( + publicIdentity *cabin.Identity + publicIdentityKey = "core:spn/public/identity" + + publicIdentityUpdateTask *modules.Task + statusUpdateTask *modules.Task +) + +func loadPublicIdentity() (err error) { + var changed bool + + publicIdentity, changed, err = cabin.LoadIdentity(publicIdentityKey) + switch { + case err == nil: + // load was successful + log.Infof("spn/captain: loaded public hub identity %s", publicIdentity.Hub.ID) + case errors.Is(err, database.ErrNotFound): + // does not exist, create new + publicIdentity, err = cabin.CreateIdentity(module.Ctx, conf.MainMapName) + if err != nil { + return fmt.Errorf("failed to create new identity: %w", err) + } + publicIdentity.SetKey(publicIdentityKey) + changed = true + + log.Infof("spn/captain: created new public hub identity %s", publicIdentity.ID) + default: + // loading error, abort + return fmt.Errorf("failed to load public identity: %w", err) + } + + // Save to database if the identity changed. + if changed { + err = publicIdentity.Save() + if err != nil { + return fmt.Errorf("failed to save new/updated identity to database: %w", err) + } + } + + // Set available networks. + conf.SetHubNetworks( + publicIdentity.Hub.Info.IPv4 != nil, + publicIdentity.Hub.Info.IPv6 != nil, + ) + if cfgOptionBindToAdvertised() { + conf.SetBindAddr(publicIdentity.Hub.Info.IPv4, publicIdentity.Hub.Info.IPv6) + } + + // Set Home Hub before updating the hub on the map, as this would trigger a + // recalculation without a Home Hub. + ok := navigator.Main.SetHome(publicIdentity.ID, nil) + // Always update the navigator in any case in order to sync the reference to + // the active struct of the identity. + navigator.Main.UpdateHub(publicIdentity.Hub) + // Setting the Home Hub will have failed if the identidy was only just + // created - try again if it failed. + if !ok { + ok = navigator.Main.SetHome(publicIdentity.ID, nil) + if !ok { + return errors.New("failed to set self as home hub") + } + } + + return nil +} + +func prepPublicIdentityMgmt() error { + publicIdentityUpdateTask = module.NewTask( + "maintain public identity", + maintainPublicIdentity, + ) + + statusUpdateTask = module.NewTask( + "maintain public status", + maintainPublicStatus, + ).Repeat(maintainStatusInterval) + + return module.RegisterEventHook( + "config", + "config change", + "update public identity from config", + func(_ context.Context, _ interface{}) error { + // trigger update in 5 minutes + publicIdentityUpdateTask.Schedule(time.Now().Add(5 * time.Minute)) + return nil + }, + ) +} + +// TriggerHubStatusMaintenance queues the Hub status update task to be executed. +func TriggerHubStatusMaintenance() { + if statusUpdateTask != nil { + statusUpdateTask.Queue() + } +} + +func maintainPublicIdentity(ctx context.Context, task *modules.Task) error { + changed, err := publicIdentity.MaintainAnnouncement(nil, false) + if err != nil { + return fmt.Errorf("failed to maintain announcement: %w", err) + } + + if !changed { + return nil + } + + // Update on map. + navigator.Main.UpdateHub(publicIdentity.Hub) + log.Debug("spn/captain: updated own hub on map after announcement change") + + // export announcement + announcementData, err := publicIdentity.ExportAnnouncement() + if err != nil { + return fmt.Errorf("failed to export announcement: %w", err) + } + + // forward to other connected Hubs + gossipRelayMsg("", GossipHubAnnouncementMsg, announcementData) + + return nil +} + +func maintainPublicStatus(ctx context.Context, task *modules.Task) error { + // Get current lanes. + cranes := docks.GetAllAssignedCranes() + lanes := make([]*hub.Lane, 0, len(cranes)) + for _, crane := range cranes { + // Ignore private, stopped or stopping cranes. + if !crane.Public() || crane.Stopped() || crane.IsStopping() { + continue + } + + // Get measurements. + measurements := crane.ConnectedHub.GetMeasurements() + latency, _ := measurements.GetLatency() + capacity, _ := measurements.GetCapacity() + + // Add crane lane. + lanes = append(lanes, &hub.Lane{ + ID: crane.ConnectedHub.ID, + Latency: latency, + Capacity: capacity, + }) + } + // Sort Lanes for comparing. + hub.SortLanes(lanes) + + // Get system load and convert to fixed steps. + var load int + loadAvg, ok := metrics.LoadAvg15() + switch { + case !ok: + load = -1 + case loadAvg >= 1: + load = 100 + case loadAvg >= 0.95: + load = 95 + case loadAvg >= 0.8: + load = 80 + default: + load = 0 + } + if loadAvg >= 0.8 { + log.Warningf("spn/captain: publishing 15m system load average of %.2f as %d", loadAvg, load) + } + + // Set flags. + var flags []string + if !patrol.HTTPSConnectivityConfirmed() { + flags = append(flags, hub.FlagNetError) + } + // Sort Lanes for comparing. + sort.Strings(flags) + + // Run maintenance with the new data. + changed, err := publicIdentity.MaintainStatus(lanes, &load, flags, false) + if err != nil { + return fmt.Errorf("failed to maintain status: %w", err) + } + + if !changed { + return nil + } + + // Update on map. + navigator.Main.UpdateHub(publicIdentity.Hub) + log.Debug("spn/captain: updated own hub on map after status change") + + // export status + statusData, err := publicIdentity.ExportStatus() + if err != nil { + return fmt.Errorf("failed to export status: %w", err) + } + + // forward to other connected Hubs + gossipRelayMsg("", GossipHubStatusMsg, statusData) + + log.Infof( + "spn/captain: updated status with load %d and current lanes: %v", + publicIdentity.Hub.Status.Load, + publicIdentity.Hub.Status.Lanes, + ) + return nil +} + +func publishShutdownStatus() { + // Create offline status. + offlineStatusData, err := publicIdentity.MakeOfflineStatus() + if err != nil { + log.Errorf("spn/captain: failed to create offline status: %s", err) + return + } + + // Forward to other connected Hubs. + gossipRelayMsg("", GossipHubStatusMsg, offlineStatusData) + + // Leave some time for the message to broadcast. + time.Sleep(2 * time.Second) + + log.Infof("spn/captain: broadcasted offline status") +} diff --git a/spn/captain/status.go b/spn/captain/status.go new file mode 100644 index 00000000..99b6632c --- /dev/null +++ b/spn/captain/status.go @@ -0,0 +1,154 @@ +package captain + +import ( + "fmt" + "sort" + "sync" + "time" + + "github.com/safing/portbase/config" + "github.com/safing/portbase/database/record" + "github.com/safing/portbase/runtime" + "github.com/safing/portbase/utils/debug" + "github.com/safing/portmaster/service/intel/geoip" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/navigator" +) + +// SPNStatus holds SPN status information. +type SPNStatus struct { + record.Base + sync.Mutex + + Status SPNStatusName + HomeHubID string + HomeHubName string + ConnectedIP string + ConnectedTransport string + ConnectedCountry *geoip.CountryInfo + ConnectedSince *time.Time +} + +// SPNStatusName is a SPN status. +type SPNStatusName string + +// SPN Stati. +const ( + StatusFailed SPNStatusName = "failed" + StatusDisabled SPNStatusName = "disabled" + StatusConnecting SPNStatusName = "connecting" + StatusConnected SPNStatusName = "connected" +) + +var ( + spnStatus = &SPNStatus{ + Status: StatusDisabled, + } + spnStatusPushFunc runtime.PushFunc +) + +func registerSPNStatusProvider() (err error) { + spnStatus.SetKey("runtime:spn/status") + spnStatus.UpdateMeta() + spnStatusPushFunc, err = runtime.Register("spn/status", runtime.ProvideRecord(spnStatus)) + return +} + +func resetSPNStatus(statusName SPNStatusName, overrideEvenIfConnected bool) { + // Lock for updating values. + spnStatus.Lock() + defer spnStatus.Unlock() + + // Ignore when connected and not overriding + if !overrideEvenIfConnected && spnStatus.Status == StatusConnected { + return + } + + // Reset status. + spnStatus.Status = statusName + spnStatus.HomeHubID = "" + spnStatus.HomeHubName = "" + spnStatus.ConnectedIP = "" + spnStatus.ConnectedTransport = "" + spnStatus.ConnectedCountry = nil + spnStatus.ConnectedSince = nil + + // Push new status. + pushSPNStatusUpdate() +} + +// pushSPNStatusUpdate pushes an update of spnStatus, which must be locked. +func pushSPNStatusUpdate() { + spnStatus.UpdateMeta() + spnStatusPushFunc(spnStatus) +} + +// GetSPNStatus returns the current SPN status. +func GetSPNStatus() *SPNStatus { + spnStatus.Lock() + defer spnStatus.Unlock() + + return &SPNStatus{ + Status: spnStatus.Status, + HomeHubID: spnStatus.HomeHubID, + HomeHubName: spnStatus.HomeHubName, + ConnectedIP: spnStatus.ConnectedIP, + ConnectedTransport: spnStatus.ConnectedTransport, + ConnectedCountry: spnStatus.ConnectedCountry, + ConnectedSince: spnStatus.ConnectedSince, + } +} + +// AddToDebugInfo adds the SPN status to the given debug.Info. +func AddToDebugInfo(di *debug.Info) { + spnStatus.Lock() + defer spnStatus.Unlock() + + // Check if SPN module is enabled. + var moduleStatus string + spnEnabled := config.GetAsBool(CfgOptionEnableSPNKey, false) + if spnEnabled() { + moduleStatus = "enabled" + } else { + moduleStatus = "disabled" + } + + // Collect status data. + lines := make([]string, 0, 20) + lines = append(lines, fmt.Sprintf("HomeHubID: %v", spnStatus.HomeHubID)) + lines = append(lines, fmt.Sprintf("HomeHubName: %v", spnStatus.HomeHubName)) + lines = append(lines, fmt.Sprintf("HomeHubIP: %v", spnStatus.ConnectedIP)) + lines = append(lines, fmt.Sprintf("Transport: %v", spnStatus.ConnectedTransport)) + if spnStatus.ConnectedSince != nil { + lines = append(lines, fmt.Sprintf("Connected: %v ago", time.Since(*spnStatus.ConnectedSince).Round(time.Minute))) + } + lines = append(lines, "---") + lines = append(lines, fmt.Sprintf("Client: %v", conf.Client())) + lines = append(lines, fmt.Sprintf("PublicHub: %v", conf.PublicHub())) + lines = append(lines, fmt.Sprintf("HubHasIPv4: %v", conf.HubHasIPv4())) + lines = append(lines, fmt.Sprintf("HubHasIPv6: %v", conf.HubHasIPv6())) + + // Collect status data of map. + if navigator.Main != nil { + lines = append(lines, "---") + mainMapStats := navigator.Main.Stats() + lines = append(lines, fmt.Sprintf("Map %s:", navigator.Main.Name)) + lines = append(lines, fmt.Sprintf("Active Terminals: %d Hubs", mainMapStats.ActiveTerminals)) + // Collect hub states. + mapStateSummary := make([]string, 0, len(mainMapStats.States)) + for state, cnt := range mainMapStats.States { + if cnt > 0 { + mapStateSummary = append(mapStateSummary, fmt.Sprintf("State %s: %d Hubs", state, cnt)) + } + } + sort.Strings(mapStateSummary) + lines = append(lines, mapStateSummary...) + } + + // Add all data as section. + di.AddSection( + fmt.Sprintf("SPN: %s (module %s)", spnStatus.Status, moduleStatus), + debug.UseCodeSection|debug.AddContentLineBreaks, + lines..., + ) +} diff --git a/spn/conf/map.go b/spn/conf/map.go new file mode 100644 index 00000000..e720be1a --- /dev/null +++ b/spn/conf/map.go @@ -0,0 +1,17 @@ +package conf + +import ( + "flag" + + "github.com/safing/portmaster/spn/hub" +) + +// Primary Map Configuration. +var ( + MainMapName = "main" + MainMapScope = hub.ScopePublic +) + +func init() { + flag.StringVar(&MainMapName, "spn-map", "main", "set main SPN map - use only for testing") +} diff --git a/spn/conf/mode.go b/spn/conf/mode.go new file mode 100644 index 00000000..cc1248bb --- /dev/null +++ b/spn/conf/mode.go @@ -0,0 +1,30 @@ +package conf + +import ( + "github.com/tevino/abool" +) + +var ( + publicHub = abool.New() + client = abool.New() +) + +// PublicHub returns whether this is a public Hub. +func PublicHub() bool { + return publicHub.IsSet() +} + +// EnablePublicHub enables the public hub mode. +func EnablePublicHub(enable bool) { + publicHub.SetTo(enable) +} + +// Client returns whether this is a client. +func Client() bool { + return client.IsSet() +} + +// EnableClient enables the client mode. +func EnableClient(enable bool) { + client.SetTo(enable) +} diff --git a/spn/conf/networks.go b/spn/conf/networks.go new file mode 100644 index 00000000..379395c3 --- /dev/null +++ b/spn/conf/networks.go @@ -0,0 +1,110 @@ +package conf + +import ( + "net" + "sync" + + "github.com/tevino/abool" +) + +var ( + hubHasV4 = abool.New() + hubHasV6 = abool.New() +) + +// SetHubNetworks sets the available IP networks on the Hub. +func SetHubNetworks(v4, v6 bool) { + hubHasV4.SetTo(v4) + hubHasV6.SetTo(v6) +} + +// HubHasIPv4 returns whether the Hub has IPv4 support. +func HubHasIPv4() bool { + return hubHasV4.IsSet() +} + +// HubHasIPv6 returns whether the Hub has IPv6 support. +func HubHasIPv6() bool { + return hubHasV6.IsSet() +} + +var ( + bindIPv4 net.IP + bindIPv6 net.IP + bindIPLock sync.Mutex +) + +// SetBindAddr sets the preferred connect (bind) addresses. +func SetBindAddr(ip4, ip6 net.IP) { + bindIPLock.Lock() + defer bindIPLock.Unlock() + + bindIPv4 = ip4 + bindIPv6 = ip6 +} + +// BindAddrIsSet returns whether any bind address is set. +func BindAddrIsSet() bool { + bindIPLock.Lock() + defer bindIPLock.Unlock() + + return bindIPv4 != nil || bindIPv6 != nil +} + +// GetBindAddr returns an address with the preferred binding address for the +// given dial network. +// The dial network must have a suffix specifying the IP version. +func GetBindAddr(dialNetwork string) net.Addr { + bindIPLock.Lock() + defer bindIPLock.Unlock() + + switch dialNetwork { + case "ip4": + if bindIPv4 != nil { + return &net.IPAddr{IP: bindIPv4} + } + case "ip6": + if bindIPv6 != nil { + return &net.IPAddr{IP: bindIPv6} + } + case "tcp4": + if bindIPv4 != nil { + return &net.TCPAddr{IP: bindIPv4} + } + case "tcp6": + if bindIPv6 != nil { + return &net.TCPAddr{IP: bindIPv6} + } + case "udp4": + if bindIPv4 != nil { + return &net.UDPAddr{IP: bindIPv4} + } + case "udp6": + if bindIPv6 != nil { + return &net.UDPAddr{IP: bindIPv6} + } + } + + return nil +} + +// GetBindIPs returns the preferred binding IPs. +// Returns a slice with a single nil IP if no preferred binding IPs are set. +func GetBindIPs() []net.IP { + bindIPLock.Lock() + defer bindIPLock.Unlock() + + switch { + case bindIPv4 == nil && bindIPv6 == nil: + // Match most common case first. + return []net.IP{nil} + case bindIPv4 != nil && bindIPv6 != nil: + return []net.IP{bindIPv4, bindIPv6} + case bindIPv4 != nil: + return []net.IP{bindIPv4} + case bindIPv6 != nil: + return []net.IP{bindIPv6} + } + + return []net.IP{nil} +} diff --git a/spn/conf/version.go b/spn/conf/version.go new file mode 100644 index 00000000..ec5f3f03 --- /dev/null +++ b/spn/conf/version.go @@ -0,0 +1,9 @@ +package conf + +const ( + // VersionOne is the first protocol version. + VersionOne = 1 + + // CurrentVersion always holds the newest version in production. + CurrentVersion = 1 +) diff --git a/spn/crew/connect.go b/spn/crew/connect.go new file mode 100644 index 00000000..96239931 --- /dev/null +++ b/spn/crew/connect.go @@ -0,0 +1,482 @@ +package crew + +import ( + "context" + "errors" + "fmt" + "net" + "strings" + "sync" + "time" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/profile/endpoints" + "github.com/safing/portmaster/spn/access" + "github.com/safing/portmaster/spn/docks" + "github.com/safing/portmaster/spn/navigator" + "github.com/safing/portmaster/spn/terminal" +) + +// connectLock locks all routing operations to mitigate racy stuff for now. +// TODO: Find a nice way to parallelize route creation. +var connectLock sync.Mutex + +// HandleSluiceRequest handles a sluice request to build a tunnel. +func HandleSluiceRequest(connInfo *network.Connection, conn net.Conn) { + if conn == nil { + log.Debugf("spn/crew: closing tunnel for %s before starting because of shutdown", connInfo) + + // This is called within the connInfo lock. + connInfo.Failed("tunnel entry closed", "") + connInfo.SaveWhenFinished() + return + } + + t := &Tunnel{ + connInfo: connInfo, + conn: conn, + } + module.StartWorker("tunnel handler", t.connectWorker) +} + +// Tunnel represents the local information and endpoint of a data tunnel. +type Tunnel struct { + connInfo *network.Connection + conn net.Conn + + dstPin *navigator.Pin + dstTerminal terminal.Terminal + route *navigator.Route + failedTries int + stickied bool +} + +func (t *Tunnel) connectWorker(ctx context.Context) (err error) { + // Get tracing logger. + ctx, tracer := log.AddTracer(ctx) + defer tracer.Submit() + + // Save start time. + started := time.Now() + + // Check the status of the Home Hub. + home, homeTerminal := navigator.Main.GetHome() + if home == nil || homeTerminal == nil || homeTerminal.IsBeingAbandoned() { + reportConnectError(terminal.ErrUnknownError.With("home terminal is abandoned")) + + t.connInfo.Lock() + defer t.connInfo.Unlock() + t.connInfo.Failed("SPN not ready for tunneling", "") + t.connInfo.Save() + + tracer.Infof("spn/crew: not tunneling %s, as the SPN is not ready", t.connInfo) + return nil + } + + // Create path through the SPN. + err = t.establish(ctx) + if err != nil { + log.Warningf("spn/crew: failed to establish route for %s: %s", t.connInfo, err) + + // TODO: Clean this up. + t.connInfo.Lock() + defer t.connInfo.Unlock() + t.connInfo.Failed(fmt.Sprintf("SPN failed to establish route: %s", err), "") + t.connInfo.Save() + + tracer.Warningf("spn/crew: failed to establish route for %s: %s", t.connInfo, err) + return nil + } + + // Connect via established tunnel. + _, tErr := NewConnectOp(t) + if tErr != nil { + tErr = tErr.Wrap("failed to initialize tunnel") + reportConnectError(tErr) + + t.connInfo.Lock() + defer t.connInfo.Unlock() + t.connInfo.Failed(fmt.Sprintf("SPN failed to initialize data tunnel (connect op): %s", tErr.Error()), "") + t.connInfo.Save() + + // TODO: try with another route? + tracer.Warningf("spn/crew: failed to initialize data tunnel (connect op) for %s: %s", t.connInfo, err) + return tErr + } + + // Report time taken to find, build and check route and send connect request. + connectOpTTCRDurationHistogram.UpdateDuration(started) + + t.connInfo.Lock() + defer t.connInfo.Unlock() + addTunnelContextToConnection(t) + t.connInfo.Save() + + tracer.Infof("spn/crew: connected %s via %s", t.connInfo, t.dstPin.Hub) + return nil +} + +func (t *Tunnel) establish(ctx context.Context) (err error) { + var routes *navigator.Routes + + // Check if the destination sticks to a Hub. + sticksTo := getStickiedHub(t.connInfo) + switch { + case sticksTo == nil: + // Continue. + + case sticksTo.Avoid: + log.Tracer(ctx).Tracef("spn/crew: avoiding %s", sticksTo.Pin.Hub) + + // Avoid this Hub. + // TODO: Remember more than one hub to avoid. + avoidPolicy := []endpoints.Endpoint{ + &endpoints.EndpointDomain{ + OriginalValue: sticksTo.Pin.Hub.ID, + Domain: strings.ToLower(sticksTo.Pin.Hub.ID) + ".", + }, + } + + // Append to policies. + t.connInfo.TunnelOpts.Destination.HubPolicies = append(t.connInfo.TunnelOpts.Destination.HubPolicies, avoidPolicy) + + default: + log.Tracer(ctx).Tracef("spn/crew: using stickied %s", sticksTo.Pin.Hub) + + // Check if the stickied Hub has an active terminal. + dstTerminal := sticksTo.Pin.GetActiveTerminal() + if dstTerminal != nil { + t.dstPin = sticksTo.Pin + t.dstTerminal = dstTerminal + t.route = sticksTo.Route + t.stickied = true + return nil + } + + // If not, attempt to find a route to the stickied hub. + routes, err = navigator.Main.FindRouteToHub( + sticksTo.Pin.Hub.ID, + t.connInfo.TunnelOpts, + ) + if err != nil { + log.Tracer(ctx).Tracef("spn/crew: failed to find route to stickied %s: %s", sticksTo.Pin.Hub, err) + routes = nil + } else { + t.stickied = true + } + } + + // Find possible routes to destination. + if routes == nil { + log.Tracer(ctx).Trace("spn/crew: finding routes...") + routes, err = navigator.Main.FindRoutes( + t.connInfo.Entity.IP, + t.connInfo.TunnelOpts, + ) + if err != nil { + return fmt.Errorf("failed to find routes to %s: %w", t.connInfo.Entity.IP, err) + } + } + + // Check if routes are okay (again). + if len(routes.All) == 0 { + return fmt.Errorf("no routes to %s", t.connInfo.Entity.IP) + } + + // Try routes until one succeeds. + log.Tracer(ctx).Trace("spn/crew: establishing route...") + var dstPin *navigator.Pin + var dstTerminal terminal.Terminal + for tries, route := range routes.All { + dstPin, dstTerminal, err = establishRoute(route) + if err != nil { + continue + } + + // Assign route data to tunnel. + t.dstPin = dstPin + t.dstTerminal = dstTerminal + t.route = route + t.failedTries = tries + + // Push changes to Pins and return. + navigator.Main.PushPinChanges() + return nil + } + + return fmt.Errorf("failed to establish a route to %s: %w", t.connInfo.Entity.IP, err) +} + +type hopCheck struct { + pin *navigator.Pin + route *navigator.Route + expansion *docks.ExpansionTerminal + authOp *access.AuthorizeOp + pingOp *PingOp +} + +func establishRoute(route *navigator.Route) (dstPin *navigator.Pin, dstTerminal terminal.Terminal, err error) { + connectLock.Lock() + defer connectLock.Unlock() + + // Check for path length. + if len(route.Path) < 1 { + return nil, nil, errors.New("path too short") + } + + // Check for failing hubs in path. + for _, hop := range route.Path[1:] { + if hop.Pin().GetState().Has(navigator.StateFailing) { + return nil, nil, fmt.Errorf("failing hub in path: %s", hop.Pin().Hub.Name()) + } + } + + // Get home hub. + previousHop, homeTerminal := navigator.Main.GetHome() + if previousHop == nil || homeTerminal == nil { + return nil, nil, navigator.ErrHomeHubUnset + } + // Convert to interface for later use. + var previousTerminal terminal.Terminal = homeTerminal + + // Check if first hub in path is the home hub. + if route.Path[0].HubID != previousHop.Hub.ID { + return nil, nil, errors.New("path start does not match home hub") + } + + // Check if path only exists of home hub. + if len(route.Path) == 1 { + return previousHop, previousTerminal, nil + } + + // TODO: Check what needs locking. + + // Build path and save created paths. + hopChecks := make([]*hopCheck, 0, len(route.Path)-1) + for i, hop := range route.Path[1:] { + // Check if we already have a connection to the Hub. + activeTerminal := hop.Pin().GetActiveTerminal() + if activeTerminal != nil { + // Ping terminal if not recently checked. + if activeTerminal.NeedsReachableCheck(1 * time.Minute) { + pingOp, tErr := NewPingOp(activeTerminal) + if tErr.IsError() { + return nil, nil, tErr.Wrap("failed start ping to %s", hop.Pin()) + } + // Add for checking results later. + hopChecks = append(hopChecks, &hopCheck{ + pin: hop.Pin(), + route: route.CopyUpTo(i + 2), + expansion: activeTerminal, + pingOp: pingOp, + }) + } + + previousHop = hop.Pin() + previousTerminal = activeTerminal + continue + } + + // Expand to next Hub. + expansion, authOp, tErr := expand(previousTerminal, previousHop, hop.Pin()) + if tErr != nil { + return nil, nil, tErr.Wrap("failed to expand to %s", hop.Pin()) + } + + // Add for checking results later. + hopChecks = append(hopChecks, &hopCheck{ + pin: hop.Pin(), + route: route.CopyUpTo(i + 2), + expansion: expansion, + authOp: authOp, + }) + + // Save previous pin for next loop or end. + previousHop = hop.Pin() + previousTerminal = expansion + } + + // Check results. + for _, check := range hopChecks { + switch { + case check.authOp != nil: + // Wait for authOp result. + select { + case tErr := <-check.authOp.Result: + switch { + case tErr.IsError(): + // There was a network or authentication error. + check.pin.MarkAsFailingFor(3 * time.Minute) + log.Warningf("spn/crew: failed to auth to %s: %s", check.pin.Hub, tErr) + return nil, nil, tErr.Wrap("failed to authenticate to %s: %w", check.pin.Hub, tErr) + + case tErr.Is(terminal.ErrExplicitAck): + // Authentication was successful. + + default: + // Authentication was aborted. + if tErr != nil { + tErr = terminal.ErrUnknownError + } + log.Warningf("spn/crew: auth to %s aborted with %s", check.pin.Hub, tErr) + return nil, nil, tErr.Wrap("authentication to %s aborted: %w", check.pin.Hub, tErr) + } + + case <-time.After(5 * time.Second): + // Mark as failing for just a minute, until server load may be less. + check.pin.MarkAsFailingFor(1 * time.Minute) + log.Warningf("spn/crew: auth to %s timed out", check.pin.Hub) + + return nil, nil, terminal.ErrTimeout.With("waiting for auth to %s", check.pin.Hub) + } + + // Add terminal extension to the map. + check.pin.SetActiveTerminal(&navigator.PinConnection{ + Terminal: check.expansion, + Route: check.route, + }) + check.expansion.MarkReachable() + log.Infof("spn/crew: added conn to %s via %s", check.pin, check.route) + + case check.pingOp != nil: + // Wait for ping result. + select { + case tErr := <-check.pingOp.Result: + if !tErr.Is(terminal.ErrExplicitAck) { + // Mark as failing long enough to expire connections and session and shutdown connections. + // TODO: Should we forcibly disconnect instead? + // TODO: This might also be triggered if a relay fails and ends the operation. + check.pin.MarkAsFailingFor(7 * time.Minute) + // Forget about existing active terminal, re-create if needed. + check.pin.SetActiveTerminal(nil) + log.Warningf("spn/crew: failed to check reachability of %s: %s", check.pin.Hub, tErr) + + return nil, nil, tErr.Wrap("failed to check reachability of %s: %w", check.pin.Hub, tErr) + } + + case <-time.After(5 * time.Second): + // Mark as failing for just a minute, until server load may be less. + check.pin.MarkAsFailingFor(1 * time.Minute) + // Forget about existing active terminal, re-create if needed. + check.pin.SetActiveTerminal(nil) + log.Warningf("spn/crew: reachability check to %s timed out", check.pin.Hub) + + return nil, nil, terminal.ErrTimeout.With("waiting for ping to %s", check.pin.Hub) + } + + check.expansion.MarkReachable() + log.Debugf("spn/crew: checked conn to %s via %s", check.pin.Hub, check.route) + + default: + log.Errorf("spn/crew: invalid hop check for %s", check.pin.Hub) + return nil, nil, terminal.ErrInternalError.With("invalid hop check") + } + } + + // Return last hop. + return previousHop, previousTerminal, nil +} + +func expand(fromTerminal terminal.Terminal, from, to *navigator.Pin) (expansion *docks.ExpansionTerminal, authOp *access.AuthorizeOp, tErr *terminal.Error) { + expansion, tErr = docks.ExpandTo(fromTerminal, to.Hub.ID, to.Hub) + if tErr != nil { + return nil, nil, tErr.Wrap("failed to expand to %s", to.Hub) + } + + authOp, tErr = access.AuthorizeToTerminal(expansion) + if tErr != nil { + expansion.Abandon(nil) + return nil, nil, tErr.Wrap("failed to authorize") + } + + log.Infof("spn/crew: expanded to %s (from %s)", to.Hub, from.Hub) + return expansion, authOp, nil +} + +// TunnelContext holds additional information about the tunnel to be added to a +// connection. +type TunnelContext struct { + Path []*TunnelContextHop + PathCost float32 + RoutingAlg string + + tunnel *Tunnel +} + +// GetExitNodeID returns the ID of the exit node. +// It returns an empty string in case no path exists. +func (tc *TunnelContext) GetExitNodeID() string { + if len(tc.Path) == 0 { + return "" + } + + return tc.Path[len(tc.Path)-1].ID +} + +// StopTunnel stops the tunnel. +func (tc *TunnelContext) StopTunnel() error { + if tc.tunnel != nil && tc.tunnel.conn != nil { + return tc.tunnel.conn.Close() + } + return nil +} + +// TunnelContextHop holds hop data for TunnelContext. +type TunnelContextHop struct { + ID string + Name string + IPv4 *TunnelContextHopIPInfo `json:",omitempty"` + IPv6 *TunnelContextHopIPInfo `json:",omitempty"` +} + +// TunnelContextHopIPInfo holds hop IP data for TunnelContextHop. +type TunnelContextHopIPInfo struct { + IP net.IP + Country string + ASN uint + ASOwner string +} + +func addTunnelContextToConnection(t *Tunnel) { + // Create and add basic info. + tunnelCtx := &TunnelContext{ + Path: make([]*TunnelContextHop, len(t.route.Path)), + PathCost: t.route.TotalCost, + RoutingAlg: t.route.Algorithm, + tunnel: t, + } + t.connInfo.TunnelContext = tunnelCtx + + // Add path info. + for i, hop := range t.route.Path { + // Add hub info. + hopCtx := &TunnelContextHop{ + ID: hop.HubID, + Name: hop.Pin().Hub.Info.Name, + } + tunnelCtx.Path[i] = hopCtx + // Add hub IPv4 info. + if hop.Pin().Hub.Info.IPv4 != nil { + hopCtx.IPv4 = &TunnelContextHopIPInfo{ + IP: hop.Pin().Hub.Info.IPv4, + } + if hop.Pin().LocationV4 != nil { + hopCtx.IPv4.Country = hop.Pin().LocationV4.Country.Code + hopCtx.IPv4.ASN = hop.Pin().LocationV4.AutonomousSystemNumber + hopCtx.IPv4.ASOwner = hop.Pin().LocationV4.AutonomousSystemOrganization + } + } + // Add hub IPv6 info. + if hop.Pin().Hub.Info.IPv6 != nil { + hopCtx.IPv6 = &TunnelContextHopIPInfo{ + IP: hop.Pin().Hub.Info.IPv6, + } + if hop.Pin().LocationV6 != nil { + hopCtx.IPv6.Country = hop.Pin().LocationV6.Country.Code + hopCtx.IPv6.ASN = hop.Pin().LocationV6.AutonomousSystemNumber + hopCtx.IPv6.ASOwner = hop.Pin().LocationV6.AutonomousSystemOrganization + } + } + } +} diff --git a/spn/crew/metrics.go b/spn/crew/metrics.go new file mode 100644 index 00000000..b9549d1e --- /dev/null +++ b/spn/crew/metrics.go @@ -0,0 +1,223 @@ +package crew + +import ( + "sync/atomic" + + "github.com/tevino/abool" + + "github.com/safing/portbase/api" + "github.com/safing/portbase/metrics" +) + +var ( + connectOpCnt *metrics.Counter + connectOpCntError *metrics.Counter + connectOpCntBadRequest *metrics.Counter + connectOpCntCanceled *metrics.Counter + connectOpCntFailed *metrics.Counter + connectOpCntConnected *metrics.Counter + connectOpCntRateLimited *metrics.Counter + + connectOpIncomingBytes *metrics.Counter + connectOpOutgoingBytes *metrics.Counter + + connectOpTTCRDurationHistogram *metrics.Histogram + connectOpTTFBDurationHistogram *metrics.Histogram + connectOpDurationHistogram *metrics.Histogram + connectOpIncomingDataHistogram *metrics.Histogram + connectOpOutgoingDataHistogram *metrics.Histogram + + metricsRegistered = abool.New() +) + +func registerMetrics() (err error) { + // Only register metrics once. + if !metricsRegistered.SetToIf(false, true) { + return nil + } + + // Connect Op Stats on client. + + connectOpCnt, err = metrics.NewCounter( + "spn/op/connect/total", + nil, + &metrics.Options{ + Name: "SPN Total Connect Operations", + InternalID: "spn_connect_count", + Permission: api.PermitUser, + Persist: true, + }, + ) + if err != nil { + return err + } + + // Connect Op Stats on server. + + connectOpCntOptions := &metrics.Options{ + Name: "SPN Total Connect Operations", + Permission: api.PermitUser, + Persist: true, + } + + connectOpCntError, err = metrics.NewCounter( + "spn/op/connect/total", + map[string]string{"result": "error"}, + connectOpCntOptions, + ) + if err != nil { + return err + } + + connectOpCntBadRequest, err = metrics.NewCounter( + "spn/op/connect/total", + map[string]string{"result": "bad_request"}, + connectOpCntOptions, + ) + if err != nil { + return err + } + + connectOpCntCanceled, err = metrics.NewCounter( + "spn/op/connect/total", + map[string]string{"result": "canceled"}, + connectOpCntOptions, + ) + if err != nil { + return err + } + + connectOpCntFailed, err = metrics.NewCounter( + "spn/op/connect/total", + map[string]string{"result": "failed"}, + connectOpCntOptions, + ) + if err != nil { + return err + } + + connectOpCntConnected, err = metrics.NewCounter( + "spn/op/connect/total", + map[string]string{"result": "connected"}, + connectOpCntOptions, + ) + if err != nil { + return err + } + + connectOpCntRateLimited, err = metrics.NewCounter( + "spn/op/connect/total", + map[string]string{"result": "rate_limited"}, + connectOpCntOptions, + ) + if err != nil { + return err + } + + _, err = metrics.NewGauge( + "spn/op/connect/active", + nil, + getActiveConnectOpsStat, + &metrics.Options{ + Name: "SPN Active Connect Operations", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + connectOpIncomingBytes, err = metrics.NewCounter( + "spn/op/connect/incoming/bytes", + nil, + &metrics.Options{ + Name: "SPN Connect Operation Incoming Bytes", + InternalID: "spn_connect_in_bytes", + Permission: api.PermitUser, + Persist: true, + }, + ) + if err != nil { + return err + } + + connectOpOutgoingBytes, err = metrics.NewCounter( + "spn/op/connect/outgoing/bytes", + nil, + &metrics.Options{ + Name: "SPN Connect Operation Outgoing Bytes", + InternalID: "spn_connect_out_bytes", + Permission: api.PermitUser, + Persist: true, + }, + ) + if err != nil { + return err + } + + connectOpTTCRDurationHistogram, err = metrics.NewHistogram( + "spn/op/connect/histogram/ttcr/seconds", + nil, + &metrics.Options{ + Name: "SPN Connect Operation time-to-connect-request Histogram", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + connectOpTTFBDurationHistogram, err = metrics.NewHistogram( + "spn/op/connect/histogram/ttfb/seconds", + nil, + &metrics.Options{ + Name: "SPN Connect Operation time-to-first-byte (from TTCR) Histogram", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + connectOpDurationHistogram, err = metrics.NewHistogram( + "spn/op/connect/histogram/duration/seconds", + nil, + &metrics.Options{ + Name: "SPN Connect Operation Duration Histogram", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + connectOpIncomingDataHistogram, err = metrics.NewHistogram( + "spn/op/connect/histogram/incoming/bytes", + nil, + &metrics.Options{ + Name: "SPN Connect Operation Downloaded Data Histogram", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + connectOpOutgoingDataHistogram, err = metrics.NewHistogram( + "spn/op/connect/histogram/outgoing/bytes", + nil, + &metrics.Options{ + Name: "SPN Connect Operation Outgoing Data Histogram", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + return nil +} + +func getActiveConnectOpsStat() float64 { + return float64(atomic.LoadInt64(activeConnectOps)) +} diff --git a/spn/crew/module.go b/spn/crew/module.go new file mode 100644 index 00000000..10d4ebed --- /dev/null +++ b/spn/crew/module.go @@ -0,0 +1,44 @@ +package crew + +import ( + "time" + + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/spn/terminal" +) + +var module *modules.Module + +func init() { + module = modules.Register("crew", nil, start, stop, "terminal", "docks", "navigator", "intel", "cabin") +} + +func start() error { + module.NewTask("sticky cleaner", cleanStickyHubs). + Repeat(10 * time.Minute) + + return registerMetrics() +} + +func stop() error { + clearStickyHubs() + terminal.StopScheduler() + + return nil +} + +var connectErrors = make(chan *terminal.Error, 10) + +func reportConnectError(tErr *terminal.Error) { + select { + case connectErrors <- tErr: + default: + } +} + +// ConnectErrors returns errors of connect operations. +// It only has a small and shared buffer and may only be used for indications, +// not for full monitoring. +func ConnectErrors() <-chan *terminal.Error { + return connectErrors +} diff --git a/spn/crew/module_test.go b/spn/crew/module_test.go new file mode 100644 index 00000000..7c0a7ad7 --- /dev/null +++ b/spn/crew/module_test.go @@ -0,0 +1,13 @@ +package crew + +import ( + "testing" + + "github.com/safing/portmaster/service/core/pmtesting" + "github.com/safing/portmaster/spn/conf" +) + +func TestMain(m *testing.M) { + conf.EnablePublicHub(true) + pmtesting.TestMain(m, module) +} diff --git a/spn/crew/op_connect.go b/spn/crew/op_connect.go new file mode 100644 index 00000000..df5e4dbf --- /dev/null +++ b/spn/crew/op_connect.go @@ -0,0 +1,585 @@ +package crew + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "strconv" + "sync/atomic" + "time" + + "github.com/safing/portbase/container" + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/terminal" +) + +// ConnectOpType is the type ID for the connection operation. +const ConnectOpType string = "connect" + +var activeConnectOps = new(int64) + +// ConnectOp is used to connect data tunnels to servers on the Internet. +type ConnectOp struct { + terminal.OperationBase + + // Flow Control + dfq *terminal.DuplexFlowQueue + + // Context and shutdown handling + // ctx is the context of the Terminal. + ctx context.Context + // cancelCtx cancels ctx. + cancelCtx context.CancelFunc + // doneWriting signals that the writer has finished writing. + doneWriting chan struct{} + + // Metrics + incomingTraffic atomic.Uint64 + outgoingTraffic atomic.Uint64 + started time.Time + + // Connection + t terminal.Terminal + conn net.Conn + request *ConnectRequest + entry bool + tunnel *Tunnel +} + +// Type returns the type ID. +func (op *ConnectOp) Type() string { + return ConnectOpType +} + +// Ctx returns the operation context. +func (op *ConnectOp) Ctx() context.Context { + return op.ctx +} + +// ConnectRequest holds all the information necessary for a connect operation. +type ConnectRequest struct { + Domain string `json:"d,omitempty"` + IP net.IP `json:"ip,omitempty"` + UsePriorityDataMsgs bool `json:"pr,omitempty"` + Protocol packet.IPProtocol `json:"p,omitempty"` + Port uint16 `json:"po,omitempty"` + QueueSize uint32 `json:"qs,omitempty"` +} + +// DialNetwork returns the address of the connect request. +func (r *ConnectRequest) DialNetwork() string { + if ip4 := r.IP.To4(); ip4 != nil { + switch r.Protocol { //nolint:exhaustive // Only looking for supported protocols. + case packet.TCP: + return "tcp4" + case packet.UDP: + return "udp4" + } + } else { + switch r.Protocol { //nolint:exhaustive // Only looking for supported protocols. + case packet.TCP: + return "tcp6" + case packet.UDP: + return "udp6" + } + } + + return "" +} + +// Address returns the address of the connext request. +func (r *ConnectRequest) Address() string { + return net.JoinHostPort(r.IP.String(), strconv.Itoa(int(r.Port))) +} + +func (r *ConnectRequest) String() string { + if r.Domain != "" { + return fmt.Sprintf("%s (%s %s)", r.Domain, r.Protocol, r.Address()) + } + return fmt.Sprintf("%s %s", r.Protocol, r.Address()) +} + +func init() { + terminal.RegisterOpType(terminal.OperationFactory{ + Type: ConnectOpType, + Requires: terminal.MayConnect, + Start: startConnectOp, + }) +} + +// NewConnectOp starts a new connect operation. +func NewConnectOp(tunnel *Tunnel) (*ConnectOp, *terminal.Error) { + // Submit metrics. + connectOpCnt.Inc() + + // Create request. + request := &ConnectRequest{ + Domain: tunnel.connInfo.Entity.Domain, + IP: tunnel.connInfo.Entity.IP, + Protocol: packet.IPProtocol(tunnel.connInfo.Entity.Protocol), + Port: tunnel.connInfo.Entity.Port, + UsePriorityDataMsgs: terminal.UsePriorityDataMsgs, + } + + // Set defaults. + if request.QueueSize == 0 { + request.QueueSize = terminal.DefaultQueueSize + } + + // Create new op. + op := &ConnectOp{ + doneWriting: make(chan struct{}), + t: tunnel.dstTerminal, + conn: tunnel.conn, + request: request, + entry: true, + tunnel: tunnel, + } + op.ctx, op.cancelCtx = context.WithCancel(module.Ctx) + op.dfq = terminal.NewDuplexFlowQueue(op.Ctx(), request.QueueSize, op.submitUpstream) + + // Prepare init msg. + data, err := dsd.Dump(request, dsd.CBOR) + if err != nil { + return nil, terminal.ErrInternalError.With("failed to pack connect request: %w", err) + } + + // Initialize. + tErr := op.t.StartOperation(op, container.New(data), 5*time.Second) + if err != nil { + return nil, tErr + } + + // Setup metrics. + op.started = time.Now() + + module.StartWorker("connect op conn reader", op.connReader) + module.StartWorker("connect op conn writer", op.connWriter) + module.StartWorker("connect op flow handler", op.dfq.FlowHandler) + + log.Infof("spn/crew: connected to %s via %s", request, tunnel.dstPin.Hub) + return op, nil +} + +func startConnectOp(t terminal.Terminal, opID uint32, data *container.Container) (terminal.Operation, *terminal.Error) { + // Check if we are running a public hub. + if !conf.PublicHub() { + return nil, terminal.ErrPermissionDenied.With("connecting is only allowed on public hubs") + } + + // Parse connect request. + request := &ConnectRequest{} + _, err := dsd.Load(data.CompileData(), request) + if err != nil { + connectOpCntError.Inc() // More like a protocol/system error than a bad request. + return nil, terminal.ErrMalformedData.With("failed to parse connect request: %w", err) + } + if request.QueueSize == 0 || request.QueueSize > terminal.MaxQueueSize { + connectOpCntError.Inc() // More like a protocol/system error than a bad request. + return nil, terminal.ErrInvalidOptions.With("invalid queue size of %d", request.QueueSize) + } + + // Check if IP seems valid. + if len(request.IP) != net.IPv4len && len(request.IP) != net.IPv6len { + connectOpCntError.Inc() // More like a protocol/system error than a bad request. + return nil, terminal.ErrInvalidOptions.With("ip address is not valid") + } + + // Create and initialize operation. + op := &ConnectOp{ + doneWriting: make(chan struct{}), + t: t, + request: request, + } + op.InitOperationBase(t, opID) + op.ctx, op.cancelCtx = context.WithCancel(t.Ctx()) + op.dfq = terminal.NewDuplexFlowQueue(op.Ctx(), request.QueueSize, op.submitUpstream) + + // Start worker to complete setting up the connection. + module.StartWorker("connect op setup", op.handleSetup) + + return op, nil +} + +func (op *ConnectOp) handleSetup(_ context.Context) error { + // Get terminal session for rate limiting. + var session *terminal.Session + if sessionTerm, ok := op.t.(terminal.SessionTerminal); ok { + session = sessionTerm.GetSession() + } else { + connectOpCntError.Inc() + log.Errorf("spn/crew: %T is not a session terminal, aborting op %s#%d", op.t, op.t.FmtID(), op.ID()) + op.Stop(op, terminal.ErrInternalError.With("no session available")) + return nil + } + + // Limit concurrency of connecting. + cancelErr := session.LimitConcurrency(op.Ctx(), func() { + op.setup(session) + }) + + // If context was canceled, stop operation. + if cancelErr != nil { + connectOpCntCanceled.Inc() + op.Stop(op, terminal.ErrCanceled.With(cancelErr.Error())) + } + + // Do not return a worker error. + return nil +} + +func (op *ConnectOp) setup(session *terminal.Session) { + // Rate limit before connecting. + if tErr := session.RateLimit(); tErr != nil { + // Add rate limit info to error. + if tErr.Is(terminal.ErrRateLimited) { + connectOpCntRateLimited.Inc() + op.Stop(op, tErr.With(session.RateLimitInfo())) + return + } + + connectOpCntError.Inc() + op.Stop(op, tErr) + return + } + + // Check if connection target is in global scope. + ipScope := netutils.GetIPScope(op.request.IP) + if ipScope != netutils.Global { + session.ReportSuspiciousActivity(terminal.SusFactorQuiteUnusual) + connectOpCntBadRequest.Inc() + op.Stop(op, terminal.ErrPermissionDenied.With("denied request to connect to non-global IP %s", op.request.IP)) + return + } + + // Check exit policy. + if tErr := checkExitPolicy(op.request); tErr != nil { + session.ReportSuspiciousActivity(terminal.SusFactorQuiteUnusual) + connectOpCntBadRequest.Inc() + op.Stop(op, tErr) + return + } + + // Check one last time before connecting if operation was not canceled. + if op.Ctx().Err() != nil { + op.Stop(op, terminal.ErrCanceled.With(op.Ctx().Err().Error())) + connectOpCntCanceled.Inc() + return + } + + // Connect to destination. + dialNet := op.request.DialNetwork() + if dialNet == "" { + session.ReportSuspiciousActivity(terminal.SusFactorCommon) + connectOpCntBadRequest.Inc() + op.Stop(op, terminal.ErrIncorrectUsage.With("protocol %s is not supported", op.request.Protocol)) + return + } + dialer := &net.Dialer{ + Timeout: 10 * time.Second, + LocalAddr: conf.GetBindAddr(dialNet), + FallbackDelay: -1, // Disables Fast Fallback from IPv6 to IPv4. + KeepAlive: -1, // Disable keep-alive. + } + conn, err := dialer.DialContext(op.Ctx(), dialNet, op.request.Address()) + if err != nil { + // Connection errors are common, but still a bit suspicious. + var netError net.Error + switch { + case errors.As(err, &netError) && netError.Timeout(): + session.ReportSuspiciousActivity(terminal.SusFactorCommon) + connectOpCntFailed.Inc() + case errors.Is(err, context.Canceled): + session.ReportSuspiciousActivity(terminal.SusFactorCommon) + connectOpCntCanceled.Inc() + default: + session.ReportSuspiciousActivity(terminal.SusFactorWeirdButOK) + connectOpCntFailed.Inc() + } + + op.Stop(op, terminal.ErrConnectionError.With("failed to connect to %s: %w", op.request, err)) + return + } + op.conn = conn + + // Start worker. + module.StartWorker("connect op conn reader", op.connReader) + module.StartWorker("connect op conn writer", op.connWriter) + module.StartWorker("connect op flow handler", op.dfq.FlowHandler) + + connectOpCntConnected.Inc() + log.Infof("spn/crew: connected op %s#%d to %s", op.t.FmtID(), op.ID(), op.request) +} + +func (op *ConnectOp) submitUpstream(msg *terminal.Msg, timeout time.Duration) { + err := op.Send(msg, timeout) + if err != nil { + msg.Finish() + op.Stop(op, err.Wrap("failed to send data (op) read from %s", op.connectedType())) + } +} + +const ( + readBufSize = 1500 + + // High priority up to first 10MB. + highPrioThreshold = 10_000_000 + + // Rate limit to 128 Mbit/s after 1GB traffic. + // Do NOT use time.Sleep per packet, as it is very inaccurate and will sleep a lot longer than desired. + rateLimitThreshold = 1_000_000_000 + rateLimitMaxMbit = 128 +) + +func (op *ConnectOp) connReader(_ context.Context) error { + // Metrics setup and submitting. + atomic.AddInt64(activeConnectOps, 1) + defer func() { + atomic.AddInt64(activeConnectOps, -1) + connectOpDurationHistogram.UpdateDuration(op.started) + connectOpIncomingDataHistogram.Update(float64(op.incomingTraffic.Load())) + }() + + rateLimiter := terminal.NewRateLimiter(rateLimitMaxMbit) + + for { + // Read from connection. + buf := make([]byte, readBufSize) + n, err := op.conn.Read(buf) + if err != nil { + if errors.Is(err, io.EOF) { + op.Stop(op, terminal.ErrStopping.With("connection to %s was closed on read", op.connectedType())) + } else { + op.Stop(op, terminal.ErrConnectionError.With("failed to read from %s: %w", op.connectedType(), err)) + } + return nil + } + if n == 0 { + log.Tracef("spn/crew: connect op %s>%d read 0 bytes from %s", op.t.FmtID(), op.ID(), op.connectedType()) + continue + } + + // Submit metrics. + connectOpIncomingBytes.Add(n) + inBytes := op.incomingTraffic.Add(uint64(n)) + + // Rate limit if over threshold. + if inBytes > rateLimitThreshold { + rateLimiter.Limit(uint64(n)) + } + + // Create message from data. + msg := op.NewMsg(buf[:n]) + + // Define priority and possibly wait for slot. + switch { + case inBytes > highPrioThreshold: + msg.Unit.WaitForSlot() + case op.request.UsePriorityDataMsgs: + msg.Unit.MakeHighPriority() + } + + // Send packet. + tErr := op.dfq.Send( + msg, + 30*time.Second, + ) + if tErr != nil { + msg.Finish() + op.Stop(op, tErr.Wrap("failed to send data (dfq) from %s", op.connectedType())) + return nil + } + } +} + +// Deliver delivers a messages to the operation. +func (op *ConnectOp) Deliver(msg *terminal.Msg) *terminal.Error { + return op.dfq.Deliver(msg) +} + +func (op *ConnectOp) connWriter(_ context.Context) error { + // Metrics submitting. + defer func() { + connectOpOutgoingDataHistogram.Update(float64(op.outgoingTraffic.Load())) + }() + + defer func() { + // Signal that we are done with writing. + close(op.doneWriting) + // Close connection. + _ = op.conn.Close() + }() + + var msg *terminal.Msg + defer msg.Finish() + + rateLimiter := terminal.NewRateLimiter(rateLimitMaxMbit) + +writing: + for { + msg.Finish() + + select { + case msg = <-op.dfq.Receive(): + case <-op.ctx.Done(): + op.Stop(op, terminal.ErrCanceled) + return nil + default: + // Handle all data before also listening for the context cancel. + // This ensures all data is written properly before stopping. + select { + case msg = <-op.dfq.Receive(): + case op.doneWriting <- struct{}{}: + op.Stop(op, terminal.ErrStopping) + return nil + case <-op.ctx.Done(): + op.Stop(op, terminal.ErrCanceled) + return nil + } + } + + // TODO: Instead of compiling data here again, can we send it as in the container? + data := msg.Data.CompileData() + if len(data) == 0 { + continue writing + } + + // Submit metrics. + connectOpOutgoingBytes.Add(len(data)) + out := op.outgoingTraffic.Add(uint64(len(data))) + + // Rate limit if over threshold. + if out > rateLimitThreshold { + rateLimiter.Limit(uint64(len(data))) + } + + // Special handling after first data was received on client. + if op.entry && + out == uint64(len(data)) { + // Report time taken to receive first byte. + connectOpTTFBDurationHistogram.UpdateDuration(op.started) + + // If not stickied yet, stick destination to Hub. + if !op.tunnel.stickied { + op.tunnel.stickDestinationToHub() + } + } + + // Send all given data. + for { + n, err := op.conn.Write(data) + switch { + case err != nil: + if errors.Is(err, io.EOF) { + op.Stop(op, terminal.ErrStopping.With("connection to %s was closed on write", op.connectedType())) + } else { + op.Stop(op, terminal.ErrConnectionError.With("failed to send to %s: %w", op.connectedType(), err)) + } + return nil + case n == 0: + op.Stop(op, terminal.ErrConnectionError.With("sent 0 bytes to %s", op.connectedType())) + return nil + case n < len(data): + // If not all data was sent, try again. + log.Debugf("spn/crew: %s#%d only sent %d/%d bytes to %s", op.t.FmtID(), op.ID(), n, len(data), op.connectedType()) + data = data[n:] + default: + continue writing + } + } + } +} + +func (op *ConnectOp) connectedType() string { + if op.entry { + return "origin" + } + return "destination" +} + +// HandleStop gives the operation the ability to cleanly shut down. +// The returned error is the error to send to the other side. +// Should never be called directly. Call Stop() instead. +func (op *ConnectOp) HandleStop(err *terminal.Error) (errorToSend *terminal.Error) { + if err.IsError() { + reportConnectError(err) + } + + // If the connection has sent or received any data so far, finish the data + // flows as it makes sense. + if op.incomingTraffic.Load() > 0 || op.outgoingTraffic.Load() > 0 { + // If the op was ended locally, send all data before closing. + // If the op was ended remotely, don't bother sending remaining data. + if !err.IsExternal() { + // Flushing could mean sending a full buffer of 50000 packets. + op.dfq.Flush(5 * time.Minute) + } + + // If the op was ended remotely, write all remaining received data. + // If the op was ended locally, don't bother writing remaining data. + if err.IsExternal() { + select { + case <-op.doneWriting: + default: + select { + case <-op.doneWriting: + case <-time.After(5 * time.Second): + } + } + } + } + + // Cancel workers. + op.cancelCtx() + + // Special client-side handling. + if op.entry { + // Mark the connection as failed if there was an error and no data was sent to the app yet. + if err.IsError() && op.outgoingTraffic.Load() == 0 { + // Set connection to failed and save it to propagate the update. + c := op.tunnel.connInfo + func() { + c.Lock() + defer c.Unlock() + + if err.IsExternal() { + c.Failed(fmt.Sprintf( + "the exit node reported an error: %s", err, + ), "") + } else { + c.Failed(fmt.Sprintf( + "connection failed locally: %s", err, + ), "") + } + + c.Save() + }() + } + + // Avoid connecting to the destination via this Hub if: + // - The error is external - ie. from the server. + // - The error is a connection error. + // - No data was received. + // This indicates that there is some network level issue that we can + // possibly work around by using another exit node. + if err.IsError() && err.IsExternal() && + err.Is(terminal.ErrConnectionError) && + op.outgoingTraffic.Load() == 0 { + op.tunnel.avoidDestinationHub() + } + + // Don't leak local errors to the server. + if !err.IsExternal() { + // Change error that is reported. + return terminal.ErrStopping + } + } + + return err +} diff --git a/spn/crew/op_connect_test.go b/spn/crew/op_connect_test.go new file mode 100644 index 00000000..7205ea9a --- /dev/null +++ b/spn/crew/op_connect_test.go @@ -0,0 +1,115 @@ +package crew + +import ( + "fmt" + "net" + "net/http" + "net/url" + "testing" + "time" + + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/spn/cabin" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/navigator" + "github.com/safing/portmaster/spn/terminal" +) + +const ( + testPadding = 8 + testQueueSize = 10 +) + +func TestConnectOp(t *testing.T) { + t.Parallel() + + if testing.Short() { + t.Skip("skipping test in short mode, as it interacts with the network") + } + + // Create test terminal pair. + a, b, err := terminal.NewSimpleTestTerminalPair(0, 0, + &terminal.TerminalOpts{ + FlowControl: terminal.FlowControlDFQ, + FlowControlSize: testQueueSize, + Padding: testPadding, + }, + ) + if err != nil { + t.Fatalf("failed to create test terminal pair: %s", err) + } + + // Set up connect op. + b.GrantPermission(terminal.MayConnect) + conf.EnablePublicHub(true) + identity, err := cabin.CreateIdentity(module.Ctx, "test") + if err != nil { + t.Fatalf("failed to create identity: %s", err) + } + _, err = identity.MaintainAnnouncement(&hub.Announcement{ + Transports: []string{ + "tcp:17", + }, + Exit: []string{ + "+ * */80", + "- *", + }, + }, true) + if err != nil { + t.Fatalf("failed to update identity: %s", err) + } + EnableConnecting(identity.Hub) + + for i := 0; i < 1; i++ { + appConn, sluiceConn := net.Pipe() + _, tErr := NewConnectOp(&Tunnel{ + connInfo: &network.Connection{ + Entity: (&intel.Entity{ + Protocol: 6, + Port: 80, + Domain: "orf.at.", + IP: net.IPv4(194, 232, 104, 142), + }).Init(0), + }, + conn: sluiceConn, + dstTerminal: a, + dstPin: &navigator.Pin{ + Hub: identity.Hub, + }, + }) + if tErr != nil { + t.Fatalf("failed to start connect op: %s", tErr) + } + + // Send request. + requestURL, err := url.Parse("http://orf.at/") + if err != nil { + t.Fatalf("failed to parse request url: %s", err) + } + r := http.Request{ + Method: http.MethodHead, + URL: requestURL, + } + err = r.Write(appConn) + if err != nil { + t.Fatalf("failed to write request: %s", err) + } + + // Recv response. + data := make([]byte, 1500) + n, err := appConn.Read(data) + if err != nil { + t.Fatalf("failed to read request: %s", err) + } + if n == 0 { + t.Fatal("received empty reply") + } + + t.Log("received data:") + fmt.Println(string(data[:n])) + + time.Sleep(500 * time.Millisecond) + } +} diff --git a/spn/crew/op_ping.go b/spn/crew/op_ping.go new file mode 100644 index 00000000..84ee4f6e --- /dev/null +++ b/spn/crew/op_ping.go @@ -0,0 +1,149 @@ +package crew + +import ( + "crypto/subtle" + "time" + + "github.com/safing/portbase/container" + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portbase/rng" + "github.com/safing/portmaster/spn/terminal" +) + +const ( + // PingOpType is the type ID of the latency test operation. + PingOpType = "ping" + + pingOpNonceSize = 16 + pingOpTimeout = 3 * time.Second +) + +// PingOp is used to measure latency. +type PingOp struct { + terminal.OneOffOperationBase + + started time.Time + nonce []byte +} + +// PingOpRequest is a ping request. +type PingOpRequest struct { + Nonce []byte `json:"n,omitempty"` +} + +// PingOpResponse is a ping response. +type PingOpResponse struct { + Nonce []byte `json:"n,omitempty"` + Time time.Time `json:"t,omitempty"` +} + +// Type returns the type ID. +func (op *PingOp) Type() string { + return PingOpType +} + +func init() { + terminal.RegisterOpType(terminal.OperationFactory{ + Type: PingOpType, + Start: startPingOp, + }) +} + +// NewPingOp runs a latency test. +func NewPingOp(t terminal.Terminal) (*PingOp, *terminal.Error) { + // Generate nonce. + nonce, err := rng.Bytes(pingOpNonceSize) + if err != nil { + return nil, terminal.ErrInternalError.With("failed to generate ping nonce: %w", err) + } + + // Create operation and init. + op := &PingOp{ + started: time.Now().UTC(), + nonce: nonce, + } + op.OneOffOperationBase.Init() + + // Create request. + pingRequest, err := dsd.Dump(&PingOpRequest{ + Nonce: op.nonce, + }, dsd.CBOR) + if err != nil { + return nil, terminal.ErrInternalError.With("failed to create ping request: %w", err) + } + + // Send ping. + tErr := t.StartOperation(op, container.New(pingRequest), pingOpTimeout) + if tErr != nil { + return nil, tErr + } + + return op, nil +} + +// Deliver delivers a message to the operation. +func (op *PingOp) Deliver(msg *terminal.Msg) *terminal.Error { + defer msg.Finish() + + // Parse response. + response := &PingOpResponse{} + _, err := dsd.Load(msg.Data.CompileData(), response) + if err != nil { + return terminal.ErrMalformedData.With("failed to parse ping response: %w", err) + } + + // Check if the nonce matches. + if subtle.ConstantTimeCompare(op.nonce, response.Nonce) != 1 { + return terminal.ErrIntegrity.With("ping nonce mismatched") + } + + return terminal.ErrExplicitAck +} + +func startPingOp(t terminal.Terminal, opID uint32, data *container.Container) (terminal.Operation, *terminal.Error) { + // Parse request. + request := &PingOpRequest{} + _, err := dsd.Load(data.CompileData(), request) + if err != nil { + return nil, terminal.ErrMalformedData.With("failed to parse ping request: %w", err) + } + + // Create response. + response, err := dsd.Dump(&PingOpResponse{ + Nonce: request.Nonce, + Time: time.Now().UTC(), + }, dsd.CBOR) + if err != nil { + return nil, terminal.ErrInternalError.With("failed to create ping response: %w", err) + } + + // Send response. + msg := terminal.NewMsg(response) + msg.FlowID = opID + msg.Unit.MakeHighPriority() + if terminal.UsePriorityDataMsgs { + msg.Type = terminal.MsgTypePriorityData + } + tErr := t.Send(msg, pingOpTimeout) + if tErr != nil { + // Finish message unit on failure. + msg.Finish() + return nil, tErr.With("failed to send ping response") + } + + // Operation is just one response and finished successfully. + return nil, nil +} + +// HandleStop gives the operation the ability to cleanly shut down. +// The returned error is the error to send to the other side. +// Should never be called directly. Call Stop() instead. +func (op *PingOp) HandleStop(err *terminal.Error) (errorToSend *terminal.Error) { + // Prevent remote from sending explicit ack, as we use it as a success signal internally. + if err.Is(terminal.ErrExplicitAck) && err.IsExternal() { + err = terminal.ErrStopping.AsExternal() + } + + // Continue with usual handling of inherited base. + return op.OneOffOperationBase.HandleStop(err) +} diff --git a/spn/crew/op_ping_test.go b/spn/crew/op_ping_test.go new file mode 100644 index 00000000..f9d6dfb4 --- /dev/null +++ b/spn/crew/op_ping_test.go @@ -0,0 +1,32 @@ +package crew + +import ( + "testing" + "time" + + "github.com/safing/portmaster/spn/terminal" +) + +func TestPingOp(t *testing.T) { + t.Parallel() + + // Create test terminal pair. + a, _, err := terminal.NewSimpleTestTerminalPair(0, 0, nil) + if err != nil { + t.Fatalf("failed to create test terminal pair: %s", err) + } + + // Create ping op. + op, tErr := NewPingOp(a) + if tErr.IsError() { + t.Fatal(tErr) + } + + // Wait for result. + select { + case result := <-op.Result: + t.Logf("ping result: %s", result.Error()) + case <-time.After(pingOpTimeout): + t.Fatal("timed out") + } +} diff --git a/spn/crew/policy.go b/spn/crew/policy.go new file mode 100644 index 00000000..5a741164 --- /dev/null +++ b/spn/crew/policy.go @@ -0,0 +1,51 @@ +package crew + +import ( + "context" + "sync" + + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/service/profile/endpoints" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/terminal" +) + +var ( + connectingHubLock sync.Mutex + connectingHub *hub.Hub +) + +// EnableConnecting enables connecting from this Hub. +func EnableConnecting(my *hub.Hub) { + connectingHubLock.Lock() + defer connectingHubLock.Unlock() + + connectingHub = my +} + +func checkExitPolicy(request *ConnectRequest) *terminal.Error { + connectingHubLock.Lock() + defer connectingHubLock.Unlock() + + // Check if connect requests are allowed. + if connectingHub == nil { + return terminal.ErrPermissionDenied.With("connect requests disabled") + } + + // Create entity. + entity := (&intel.Entity{ + IP: request.IP, + Protocol: uint8(request.Protocol), + Port: request.Port, + Domain: request.Domain, + }).Init(0) + entity.FetchData(context.TODO()) + + // Check against policy. + result, reason := connectingHub.GetInfo().ExitPolicy().Match(context.TODO(), entity) + if result == endpoints.Denied { + return terminal.ErrPermissionDenied.With("connect request for %s violates the exit policy: %s", request, reason) + } + + return nil +} diff --git a/spn/crew/sticky.go b/spn/crew/sticky.go new file mode 100644 index 00000000..598476fa --- /dev/null +++ b/spn/crew/sticky.go @@ -0,0 +1,176 @@ +package crew + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/safing/portbase/log" + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/network/packet" + "github.com/safing/portmaster/spn/navigator" +) + +const ( + stickyTTL = 1 * time.Hour +) + +var ( + stickyIPs = make(map[string]*stickyHub) + stickyDomains = make(map[string]*stickyHub) + stickyLock sync.Mutex +) + +type stickyHub struct { + Pin *navigator.Pin + Route *navigator.Route + LastSeen time.Time + Avoid bool +} + +func (sh *stickyHub) isExpired() bool { + return time.Now().Add(-stickyTTL).After(sh.LastSeen) +} + +func makeStickyIPKey(conn *network.Connection) string { + if p := conn.Process().Profile(); p != nil { + return fmt.Sprintf( + "%s/%s>%s", + p.LocalProfile().Source, + p.LocalProfile().ID, + conn.Entity.IP, + ) + } + + return "?>" + string(conn.Entity.IP) +} + +func makeStickyDomainKey(conn *network.Connection) string { + if p := conn.Process().Profile(); p != nil { + return fmt.Sprintf( + "%s/%s>%s", + p.LocalProfile().Source, + p.LocalProfile().ID, + conn.Entity.Domain, + ) + } + + return "?>" + conn.Entity.Domain +} + +func getStickiedHub(conn *network.Connection) (sticksTo *stickyHub) { + stickyLock.Lock() + defer stickyLock.Unlock() + + // Check if IP is sticky. + sticksTo = stickyIPs[makeStickyIPKey(conn)] // byte comparison + if sticksTo != nil && !sticksTo.isExpired() { + sticksTo.LastSeen = time.Now() + } + + // If the IP did not stick and we have a domain, check if that sticks. + if sticksTo == nil && conn.Entity.Domain != "" { + sticksTo, ok := stickyDomains[makeStickyDomainKey(conn)] + if ok && !sticksTo.isExpired() { + sticksTo.LastSeen = time.Now() + } + } + + // If nothing sticked, return now. + if sticksTo == nil { + return nil + } + + // Get intel from map before locking pin to avoid simultaneous locking. + mapIntel := navigator.Main.GetIntel() + + // Lock Pin for checking. + sticksTo.Pin.Lock() + defer sticksTo.Pin.Unlock() + + // Check if the stickied Hub supports the needed IP version. + switch { + case conn.IPVersion == packet.IPv4 && sticksTo.Pin.EntityV4 == nil: + // Connection is IPv4, but stickied Hub has no IPv4. + return nil + case conn.IPVersion == packet.IPv6 && sticksTo.Pin.EntityV6 == nil: + // Connection is IPv4, but stickied Hub has no IPv4. + return nil + } + + // Disregard stickied Hub if it is disregard with the current options. + matcher := conn.TunnelOpts.Destination.Matcher(mapIntel) + if !matcher(sticksTo.Pin) { + return nil + } + + // Return fully checked stickied Hub. + return sticksTo +} + +func (t *Tunnel) stickDestinationToHub() { + stickyLock.Lock() + defer stickyLock.Unlock() + + // Stick to IP. + ipKey := makeStickyIPKey(t.connInfo) + stickyIPs[ipKey] = &stickyHub{ + Pin: t.dstPin, + Route: t.route, + LastSeen: time.Now(), + } + log.Infof("spn/crew: sticking %s to %s", ipKey, t.dstPin.Hub) + + // Stick to Domain, if present. + if t.connInfo.Entity.Domain != "" { + domainKey := makeStickyDomainKey(t.connInfo) + stickyDomains[domainKey] = &stickyHub{ + Pin: t.dstPin, + Route: t.route, + LastSeen: time.Now(), + } + log.Infof("spn/crew: sticking %s to %s", domainKey, t.dstPin.Hub) + } +} + +func (t *Tunnel) avoidDestinationHub() { + stickyLock.Lock() + defer stickyLock.Unlock() + + // Stick to Hub/IP Pair. + ipKey := makeStickyIPKey(t.connInfo) + stickyIPs[ipKey] = &stickyHub{ + Pin: t.dstPin, + LastSeen: time.Now(), + Avoid: true, + } + log.Warningf("spn/crew: avoiding %s for %s", t.dstPin.Hub, ipKey) +} + +func cleanStickyHubs(ctx context.Context, task *modules.Task) error { + stickyLock.Lock() + defer stickyLock.Unlock() + + for _, stickyRegistry := range []map[string]*stickyHub{stickyIPs, stickyDomains} { + for key, stickedEntry := range stickyRegistry { + if stickedEntry.isExpired() { + delete(stickyRegistry, key) + } + } + } + + return nil +} + +func clearStickyHubs() { + stickyLock.Lock() + defer stickyLock.Unlock() + + for _, stickyRegistry := range []map[string]*stickyHub{stickyIPs, stickyDomains} { + for key := range stickyRegistry { + delete(stickyRegistry, key) + } + } +} diff --git a/spn/docks/bandwidth_test.go b/spn/docks/bandwidth_test.go new file mode 100644 index 00000000..60101f1c --- /dev/null +++ b/spn/docks/bandwidth_test.go @@ -0,0 +1,90 @@ +package docks + +import ( + "testing" + "time" + + "github.com/tevino/abool" + + "github.com/safing/portbase/container" + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portmaster/spn/terminal" +) + +func TestEffectiveBandwidth(t *testing.T) { //nolint:paralleltest // Run alone. + // Skip in CI. + if testing.Short() { + t.Skip() + } + + var ( + bwTestDelay = 50 * time.Millisecond + bwTestQueueSize uint32 = 1000 + bwTestVolume = 10000000 // 10MB + bwTestTime = 10 * time.Second + ) + + // Create test terminal pair. + a, b, err := terminal.NewSimpleTestTerminalPair( + bwTestDelay, + int(bwTestQueueSize), + &terminal.TerminalOpts{ + FlowControl: terminal.FlowControlDFQ, + FlowControlSize: bwTestQueueSize, + }, + ) + if err != nil { + t.Fatalf("failed to create test terminal pair: %s", err) + } + + // Grant permission for op on remote terminal and start op. + b.GrantPermission(terminal.IsCraneController) + + // Re-use the capacity test for the bandwidth test. + op := &CapacityTestOp{ + opts: &CapacityTestOptions{ + TestVolume: bwTestVolume, + MaxTime: bwTestTime, + testing: true, + }, + recvQueue: make(chan *terminal.Msg), + dataSent: new(int64), + dataSentWasAckd: abool.New(), + result: make(chan *terminal.Error, 1), + } + // Disable sender again. + op.senderStarted = true + op.dataSentWasAckd.Set() + // Make capacity test request. + request, err := dsd.Dump(op.opts, dsd.CBOR) + if err != nil { + t.Fatal(terminal.ErrInternalError.With("failed to serialize capactity test options: %w", err)) + } + // Send test request. + tErr := a.StartOperation(op, container.New(request), 1*time.Second) + if tErr != nil { + t.Fatal(tErr) + } + // Start handler. + module.StartWorker("op capacity handler", op.handler) + + // Wait for result and check error. + tErr = <-op.Result() + if !tErr.IsOK() { + t.Fatalf("op failed: %s", tErr) + } + t.Logf("measured capacity: %d bit/s", op.testResult) + + // Calculate expected bandwidth. + expectedBitsPerSecond := (float64(capacityTestMsgSize*8*int64(bwTestQueueSize)) / float64(bwTestDelay)) * float64(time.Second) + t.Logf("expected capacity: %f bit/s", expectedBitsPerSecond) + + // Check if measured bandwidth is within parameters. + if float64(op.testResult) > expectedBitsPerSecond*1.6 { + t.Fatal("measured capacity too high") + } + // TODO: Check if we can raise this to at least 90%. + if float64(op.testResult) < expectedBitsPerSecond*0.2 { + t.Fatal("measured capacity too low") + } +} diff --git a/spn/docks/controller.go b/spn/docks/controller.go new file mode 100644 index 00000000..05e18e39 --- /dev/null +++ b/spn/docks/controller.go @@ -0,0 +1,100 @@ +package docks + +import ( + "github.com/safing/portbase/container" + "github.com/safing/portmaster/spn/terminal" +) + +// CraneControllerTerminal is a terminal for the crane itself. +type CraneControllerTerminal struct { + *terminal.TerminalBase + + Crane *Crane +} + +// NewLocalCraneControllerTerminal returns a new local crane controller. +func NewLocalCraneControllerTerminal( + crane *Crane, + initMsg *terminal.TerminalOpts, +) (*CraneControllerTerminal, *container.Container, *terminal.Error) { + // Remove unnecessary options from the crane controller. + initMsg.Padding = 0 + + // Create Terminal Base. + t, initData, err := terminal.NewLocalBaseTerminal( + crane.ctx, + 0, + crane.ID, + nil, + initMsg, + terminal.UpstreamSendFunc(crane.sendImportantTerminalMsg), + ) + if err != nil { + return nil, nil, err + } + + return initCraneController(crane, t, initMsg), initData, nil +} + +// NewRemoteCraneControllerTerminal returns a new remote crane controller. +func NewRemoteCraneControllerTerminal( + crane *Crane, + initData *container.Container, +) (*CraneControllerTerminal, *terminal.TerminalOpts, *terminal.Error) { + // Create Terminal Base. + t, initMsg, err := terminal.NewRemoteBaseTerminal( + crane.ctx, + 0, + crane.ID, + nil, + initData, + terminal.UpstreamSendFunc(crane.sendImportantTerminalMsg), + ) + if err != nil { + return nil, nil, err + } + + return initCraneController(crane, t, initMsg), initMsg, nil +} + +func initCraneController( + crane *Crane, + t *terminal.TerminalBase, + initMsg *terminal.TerminalOpts, +) *CraneControllerTerminal { + // Create Crane Terminal and assign it as the extended Terminal. + cct := &CraneControllerTerminal{ + TerminalBase: t, + Crane: crane, + } + t.SetTerminalExtension(cct) + + // Assign controller to crane. + crane.Controller = cct + crane.terminals[cct.ID()] = cct + + // Copy the options to the crane itself. + crane.opts = *initMsg + + // Grant crane controller permission. + t.GrantPermission(terminal.IsCraneController) + + // Start workers. + t.StartWorkers(module, "crane controller terminal") + + return cct +} + +// HandleAbandon gives the terminal the ability to cleanly shut down. +func (controller *CraneControllerTerminal) HandleAbandon(err *terminal.Error) (errorToSend *terminal.Error) { + // Abandon terminal. + controller.Crane.AbandonTerminal(0, err) + + return err +} + +// HandleDestruction gives the terminal the ability to clean up. +func (controller *CraneControllerTerminal) HandleDestruction(err *terminal.Error) { + // Stop controlled crane. + controller.Crane.Stop(nil) +} diff --git a/spn/docks/crane.go b/spn/docks/crane.go new file mode 100644 index 00000000..34dab6d3 --- /dev/null +++ b/spn/docks/crane.go @@ -0,0 +1,913 @@ +package docks + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "sync" + "time" + + "github.com/tevino/abool" + + "github.com/safing/jess" + "github.com/safing/portbase/container" + "github.com/safing/portbase/formats/varint" + "github.com/safing/portbase/log" + "github.com/safing/portbase/rng" + "github.com/safing/portmaster/spn/cabin" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/ships" + "github.com/safing/portmaster/spn/terminal" +) + +const ( + // QOTD holds the quote of the day to return on idling unused connections. + QOTD = "Privacy is not an option, and it shouldn't be the price we accept for just getting on the Internet.\nGary Kovacs\n" + + // maxUnloadSize defines the maximum size of a message to unload. + maxUnloadSize = 16384 + maxSegmentLength = 16384 + maxCraneStoppingDuration = 6 * time.Hour + maxCraneStopDuration = 10 * time.Second +) + +var ( + // optimalMinLoadSize defines minimum for Crane.targetLoadSize. + optimalMinLoadSize = 3072 // Targeting around 4096. + + // loadingMaxWaitDuration is the maximum time a crane will wait for + // additional data to send. + loadingMaxWaitDuration = 5 * time.Millisecond +) + +// Errors. +var ( + ErrDone = errors.New("crane is done") +) + +// Crane is the primary duplexer and connection manager. +type Crane struct { + // ID is the ID of the Crane. + ID string + // opts holds options. + opts terminal.TerminalOpts + + // ctx is the context of the Terminal. + ctx context.Context + // cancelCtx cancels ctx. + cancelCtx context.CancelFunc + // stopping indicates if the Crane will be stopped soon. The Crane may still + // be used until stopped, but must not be advertised anymore. + stopping *abool.AtomicBool + // stopped indicates if the Crane has been stopped. Whoever stopped the Crane + // already took care of notifying everyone, so a silent fail is normally the + // best response. + stopped *abool.AtomicBool + // authenticated indicates if there is has been any successful authentication. + authenticated *abool.AtomicBool + + // ConnectedHub is the identity of the remote Hub. + ConnectedHub *hub.Hub + // NetState holds the network optimization state. + // It must always be set and the reference must not be changed. + // Access to fields within are coordinated by itself. + NetState *NetworkOptimizationState + // identity is identity of this instance and is usually only populated on a server. + identity *cabin.Identity + + // jession is the jess session used for encryption. + jession *jess.Session + // jessionLock locks jession. + jessionLock sync.Mutex + + // Controller is the Crane's Controller Terminal. + Controller *CraneControllerTerminal + + // ship represents the underlying physical connection. + ship ships.Ship + // unloading moves containers from the ship to the crane. + unloading chan *container.Container + // loading moves containers from the crane to the ship. + loading chan *container.Container + // terminalMsgs holds containers from terminals waiting to be laoded. + terminalMsgs chan *terminal.Msg + // controllerMsgs holds important containers from terminals waiting to be laoded. + controllerMsgs chan *terminal.Msg + + // terminals holds all the connected terminals. + terminals map[uint32]terminal.Terminal + // terminalsLock locks terminals. + terminalsLock sync.Mutex + // nextTerminalID holds the next terminal ID. + nextTerminalID uint32 + + // targetLoadSize defines the optimal loading size. + targetLoadSize int +} + +// NewCrane returns a new crane. +func NewCrane(ship ships.Ship, connectedHub *hub.Hub, id *cabin.Identity) (*Crane, error) { + // Cranes always run in module context. + ctx, cancelCtx := context.WithCancel(module.Ctx) + + newCrane := &Crane{ + ctx: ctx, + cancelCtx: cancelCtx, + stopping: abool.NewBool(false), + stopped: abool.NewBool(false), + authenticated: abool.NewBool(false), + + ConnectedHub: connectedHub, + NetState: newNetworkOptimizationState(), + identity: id, + + ship: ship, + unloading: make(chan *container.Container), + loading: make(chan *container.Container, 100), + terminalMsgs: make(chan *terminal.Msg, 100), + controllerMsgs: make(chan *terminal.Msg, 100), + + terminals: make(map[uint32]terminal.Terminal), + } + err := registerCrane(newCrane) + if err != nil { + return nil, fmt.Errorf("failed to register crane: %w", err) + } + + // Shift next terminal IDs on the server. + if !ship.IsMine() { + newCrane.nextTerminalID += 4 + } + + // Calculate target load size. + loadSize := ship.LoadSize() + if loadSize <= 0 { + loadSize = ships.BaseMTU + } + newCrane.targetLoadSize = loadSize + for newCrane.targetLoadSize < optimalMinLoadSize { + newCrane.targetLoadSize += loadSize + } + // Subtract overhead needed for encryption. + newCrane.targetLoadSize -= 25 // Manually tested for jess.SuiteWireV1 + // Subtract space needed for length encoding the final chunk. + newCrane.targetLoadSize -= varint.EncodedSize(uint64(newCrane.targetLoadSize)) + + return newCrane, nil +} + +// IsMine returns whether the crane was started on this side. +func (crane *Crane) IsMine() bool { + return crane.ship.IsMine() +} + +// Public returns whether the crane has been published. +func (crane *Crane) Public() bool { + return crane.ship.Public() +} + +// IsStopping returns whether the crane is stopping. +func (crane *Crane) IsStopping() bool { + return crane.stopping.IsSet() +} + +// MarkStoppingRequested marks the crane as stopping requested. +func (crane *Crane) MarkStoppingRequested() { + crane.NetState.lock.Lock() + defer crane.NetState.lock.Unlock() + + if !crane.NetState.stoppingRequested { + crane.NetState.stoppingRequested = true + crane.startSyncStateOp() + } +} + +// MarkStopping marks the crane as stopping. +func (crane *Crane) MarkStopping() (stopping bool) { + // Can only stop owned cranes. + if !crane.IsMine() { + return false + } + + if !crane.stopping.SetToIf(false, true) { + return false + } + + crane.NetState.lock.Lock() + defer crane.NetState.lock.Unlock() + crane.NetState.markedStoppingAt = time.Now() + + crane.startSyncStateOp() + return true +} + +// AbortStopping aborts the stopping. +func (crane *Crane) AbortStopping() (aborted bool) { + aborted = crane.stopping.SetToIf(true, false) + + crane.NetState.lock.Lock() + defer crane.NetState.lock.Unlock() + + abortedStoppingRequest := crane.NetState.stoppingRequested + crane.NetState.stoppingRequested = false + crane.NetState.markedStoppingAt = time.Time{} + + // Sync if any state changed. + if aborted || abortedStoppingRequest { + crane.startSyncStateOp() + } + + return aborted +} + +// Authenticated returns whether the other side of the crane has authenticated +// itself with an access code. +func (crane *Crane) Authenticated() bool { + return crane.authenticated.IsSet() +} + +// Publish publishes the connection as a lane. +func (crane *Crane) Publish() error { + // Check if crane is connected. + if crane.ConnectedHub == nil { + return fmt.Errorf("spn/docks: %s: cannot publish without defined connected hub", crane) + } + + // Submit metrics. + if !crane.Public() { + newPublicCranes.Inc() + } + + // Mark crane as public. + maskedID := crane.ship.MaskAddress(crane.ship.RemoteAddr()) + crane.ship.MarkPublic() + + // Assign crane to make it available to others. + AssignCrane(crane.ConnectedHub.ID, crane) + + log.Infof("spn/docks: %s (was %s) is now public", crane, maskedID) + return nil +} + +// LocalAddr returns ship's local address. +func (crane *Crane) LocalAddr() net.Addr { + return crane.ship.LocalAddr() +} + +// RemoteAddr returns ship's local address. +func (crane *Crane) RemoteAddr() net.Addr { + return crane.ship.RemoteAddr() +} + +// Transport returns ship's transport. +func (crane *Crane) Transport() *hub.Transport { + return crane.ship.Transport() +} + +func (crane *Crane) getNextTerminalID() uint32 { + crane.terminalsLock.Lock() + defer crane.terminalsLock.Unlock() + + for { + // Bump to next ID. + crane.nextTerminalID += 8 + + // Check if it's free. + _, ok := crane.terminals[crane.nextTerminalID] + if !ok { + return crane.nextTerminalID + } + } +} + +func (crane *Crane) terminalCount() int { + crane.terminalsLock.Lock() + defer crane.terminalsLock.Unlock() + + return len(crane.terminals) +} + +func (crane *Crane) getTerminal(id uint32) (t terminal.Terminal, ok bool) { + crane.terminalsLock.Lock() + defer crane.terminalsLock.Unlock() + + t, ok = crane.terminals[id] + return +} + +func (crane *Crane) setTerminal(t terminal.Terminal) { + crane.terminalsLock.Lock() + defer crane.terminalsLock.Unlock() + + crane.terminals[t.ID()] = t +} + +func (crane *Crane) deleteTerminal(id uint32) (t terminal.Terminal, ok bool) { + crane.terminalsLock.Lock() + defer crane.terminalsLock.Unlock() + + t, ok = crane.terminals[id] + if ok { + delete(crane.terminals, id) + return t, true + } + return nil, false +} + +// AbandonTerminal abandons the terminal with the given ID. +func (crane *Crane) AbandonTerminal(id uint32, err *terminal.Error) { + // Get active terminal. + t, ok := crane.deleteTerminal(id) + if ok { + // If the terminal was registered, abandon it. + + // Log reason the terminal is ending. Override stopping error with nil. + switch { + case err == nil || err.IsOK(): + log.Debugf("spn/docks: %T %s is being abandoned", t, t.FmtID()) + case err.Is(terminal.ErrStopping): + err = nil + log.Debugf("spn/docks: %T %s is being abandoned by peer", t, t.FmtID()) + case err.Is(terminal.ErrNoActivity): + err = nil + log.Debugf("spn/docks: %T %s is being abandoned due to no activity", t, t.FmtID()) + default: + log.Warningf("spn/docks: %T %s: %s", t, t.FmtID(), err) + } + + // Call the terminal's abandon function. + t.Abandon(err) + } else { //nolint:gocritic + // When a crane terminal is abandoned, it calls crane.AbandonTerminal when + // finished. This time, the terminal won't be in the registry anymore and + // it finished shutting down, so we can now check if the crane needs to be + // stopped. + + // If the crane is stopping, check if we can stop. + // We can stop when all terminals are abandoned or after a timeout. + // FYI: The crane controller will always take up one slot. + if crane.stopping.IsSet() && + crane.terminalCount() <= 1 { + // Stop the crane in worker, so the caller can do some work. + module.StartWorker("retire crane", func(_ context.Context) error { + // Let enough time for the last errors to be sent, as terminals are abandoned in a goroutine. + time.Sleep(3 * time.Second) + crane.Stop(nil) + return nil + }) + } + } +} + +func (crane *Crane) sendImportantTerminalMsg(msg *terminal.Msg, timeout time.Duration) *terminal.Error { + select { + case crane.controllerMsgs <- msg: + return nil + case <-crane.ctx.Done(): + msg.Finish() + return terminal.ErrCanceled + } +} + +// Send is used by others to send a message through the crane. +func (crane *Crane) Send(msg *terminal.Msg, timeout time.Duration) *terminal.Error { + select { + case crane.terminalMsgs <- msg: + return nil + case <-crane.ctx.Done(): + msg.Finish() + return terminal.ErrCanceled + } +} + +func (crane *Crane) encrypt(shipment *container.Container) (encrypted *container.Container, err error) { + // Skip if encryption is not enabled. + if crane.jession == nil { + return shipment, nil + } + + crane.jessionLock.Lock() + defer crane.jessionLock.Unlock() + + letter, err := crane.jession.Close(shipment.CompileData()) + if err != nil { + return nil, err + } + + encrypted, err = letter.ToWire() + if err != nil { + return nil, fmt.Errorf("failed to pack letter: %w", err) + } + + return encrypted, nil +} + +func (crane *Crane) decrypt(shipment *container.Container) (decrypted *container.Container, err error) { + // Skip if encryption is not enabled. + if crane.jession == nil { + return shipment, nil + } + + crane.jessionLock.Lock() + defer crane.jessionLock.Unlock() + + letter, err := jess.LetterFromWire(shipment) + if err != nil { + return nil, fmt.Errorf("failed to parse letter: %w", err) + } + + decryptedData, err := crane.jession.Open(letter) + if err != nil { + return nil, err + } + + return container.New(decryptedData), nil +} + +func (crane *Crane) unloader(workerCtx context.Context) error { + // Unclean shutdown safeguard. + defer crane.Stop(terminal.ErrUnknownError.With("unloader died")) + + for { + // Get first couple bytes to get the packet length. + // 2 bytes are enough to encode 65535. + // On the other hand, packets can be only 2 bytes small. + lenBuf := make([]byte, 2) + err := crane.unloadUntilFull(lenBuf) + if err != nil { + if errors.Is(err, io.EOF) { + crane.Stop(terminal.ErrStopping.With("connection closed")) + } else { + crane.Stop(terminal.ErrInternalError.With("failed to unload: %w", err)) + } + return nil + } + + // Unpack length. + containerLen, n, err := varint.Unpack64(lenBuf) + if err != nil { + crane.Stop(terminal.ErrMalformedData.With("failed to get container length: %w", err)) + return nil + } + switch { + case containerLen <= 0: + crane.Stop(terminal.ErrMalformedData.With("received empty container with length %d", containerLen)) + return nil + case containerLen > maxUnloadSize: + crane.Stop(terminal.ErrMalformedData.With("received oversized container with length %d", containerLen)) + return nil + } + + // Build shipment. + var shipmentBuf []byte + leftovers := len(lenBuf) - n + + if leftovers == int(containerLen) { + // We already have all the shipment data. + shipmentBuf = lenBuf[n:] + } else { + // Create a shipment buffer, copy leftovers and read the rest from the connection. + shipmentBuf = make([]byte, containerLen) + if leftovers > 0 { + copy(shipmentBuf, lenBuf[n:]) + } + + // Read remaining shipment. + err = crane.unloadUntilFull(shipmentBuf[leftovers:]) + if err != nil { + crane.Stop(terminal.ErrInternalError.With("failed to unload: %w", err)) + return nil + } + } + + // Submit to handler. + select { + case <-crane.ctx.Done(): + crane.Stop(nil) + return nil + case crane.unloading <- container.New(shipmentBuf): + } + } +} + +func (crane *Crane) unloadUntilFull(buf []byte) error { + var bytesRead int + for { + // Get shipment from ship. + n, err := crane.ship.UnloadTo(buf[bytesRead:]) + if err != nil { + return err + } + if n == 0 { + log.Tracef("spn/docks: %s unloaded 0 bytes", crane) + } + bytesRead += n + + // Return if buffer has been fully filled. + if bytesRead == len(buf) { + // Submit metrics. + crane.submitCraneTrafficStats(bytesRead) + crane.NetState.ReportTraffic(uint64(bytesRead), true) + + return nil + } + } +} + +func (crane *Crane) handler(workerCtx context.Context) error { + var partialShipment *container.Container + var segmentLength uint32 + + // Unclean shutdown safeguard. + defer crane.Stop(terminal.ErrUnknownError.With("handler died")) + +handling: + for { + select { + case <-crane.ctx.Done(): + crane.Stop(nil) + return nil + + case shipment := <-crane.unloading: + // log.Debugf("spn/crane %s: before decrypt: %v ... %v", crane.ID, c.CompileData()[:10], c.CompileData()[c.Length()-10:]) + + // Decrypt shipment. + shipment, err := crane.decrypt(shipment) + if err != nil { + crane.Stop(terminal.ErrIntegrity.With("failed to decrypt: %w", err)) + return nil + } + + // Process all segments/containers of the shipment. + for shipment.HoldsData() { + if partialShipment != nil { + // Continue processing partial segment. + // Append new shipment to previous partial segment. + partialShipment.AppendContainer(shipment) + shipment, partialShipment = partialShipment, nil + } + + // Get next segment length. + if segmentLength == 0 { + segmentLength, err = shipment.GetNextN32() + if err != nil { + if errors.Is(err, varint.ErrBufTooSmall) { + // Continue handling when there is not yet enough data. + partialShipment = shipment + segmentLength = 0 + continue handling + } + + crane.Stop(terminal.ErrMalformedData.With("failed to get segment length: %w", err)) + return nil + } + + if segmentLength == 0 { + // Remainder is padding. + continue handling + } + + // Check if the segment is within the boundary. + if segmentLength > maxSegmentLength { + crane.Stop(terminal.ErrMalformedData.With("received oversized segment with length %d", segmentLength)) + return nil + } + } + + // Check if we have enough data for the segment. + if uint32(shipment.Length()) < segmentLength { + partialShipment = shipment + continue handling + } + + // Get segment from shipment. + segment, err := shipment.GetAsContainer(int(segmentLength)) + if err != nil { + crane.Stop(terminal.ErrMalformedData.With("failed to get segment: %w", err)) + return nil + } + segmentLength = 0 + + // Get terminal ID and message type of segment. + terminalID, terminalMsgType, err := terminal.ParseIDType(segment) + if err != nil { + crane.Stop(terminal.ErrMalformedData.With("failed to get terminal ID and msg type: %w", err)) + return nil + } + + switch terminalMsgType { + case terminal.MsgTypeInit: + crane.establishTerminal(terminalID, segment) + + case terminal.MsgTypeData, terminal.MsgTypePriorityData: + // Get terminal and let it further handle the message. + t, ok := crane.getTerminal(terminalID) + if ok { + // Create msg and set priority. + msg := terminal.NewEmptyMsg() + msg.FlowID = terminalID + msg.Type = terminalMsgType + msg.Data = segment + if msg.Type == terminal.MsgTypePriorityData { + msg.Unit.MakeHighPriority() + } + // Deliver to terminal. + deliveryErr := t.Deliver(msg) + if deliveryErr != nil { + msg.Finish() + // This is a hot path. Start a worker for abandoning the terminal. + module.StartWorker("end terminal", func(_ context.Context) error { + crane.AbandonTerminal(t.ID(), deliveryErr.Wrap("failed to deliver data")) + return nil + }) + } + } else { + log.Tracef("spn/docks: %s received msg for unknown terminal %d", crane, terminalID) + } + + case terminal.MsgTypeStop: + // Parse error. + receivedErr, err := terminal.ParseExternalError(segment.CompileData()) + if err != nil { + log.Warningf("spn/docks: %s failed to parse abandon error: %s", crane, err) + receivedErr = terminal.ErrUnknownError.AsExternal() + } + // This is a hot path. Start a worker for abandoning the terminal. + module.StartWorker("end terminal", func(_ context.Context) error { + crane.AbandonTerminal(terminalID, receivedErr) + return nil + }) + } + } + } + } +} + +func (crane *Crane) loader(workerCtx context.Context) (err error) { + shipment := container.New() + var partialShipment *container.Container + var loadingTimer *time.Timer + + // Unclean shutdown safeguard. + defer crane.Stop(terminal.ErrUnknownError.With("loader died")) + + // Return the loading wait channel if waiting. + loadNow := func() <-chan time.Time { + if loadingTimer != nil { + return loadingTimer.C + } + return nil + } + + // Make sure any received message is finished + var msg, firstMsg *terminal.Msg + defer msg.Finish() + defer firstMsg.Finish() + + for { + // Reset first message in shipment. + firstMsg.Finish() + firstMsg = nil + + fillingShipment: + for shipment.Length() < crane.targetLoadSize { + // Gather segments until shipment is filled. + + // Prioritize messages from the controller. + select { + case msg = <-crane.controllerMsgs: + case <-crane.ctx.Done(): + crane.Stop(nil) + return nil + + default: + // Then listen for all. + select { + case msg = <-crane.controllerMsgs: + case msg = <-crane.terminalMsgs: + case <-loadNow(): + break fillingShipment + case <-crane.ctx.Done(): + crane.Stop(nil) + return nil + } + } + + // Debug unit leaks. + msg.Debug() + + // Handle new message. + if msg != nil { + // Pack msg and add to segment. + msg.Pack() + newSegment := msg.Data + + // Check if this is the first message. + // This is the only message where we wait for a slot. + if firstMsg == nil { + firstMsg = msg + firstMsg.Unit.WaitForSlot() + } else { + msg.Finish() + } + + // Check length. + if newSegment.Length() > maxSegmentLength { + log.Warningf("spn/docks: %s ignored oversized segment with length %d", crane, newSegment.Length()) + continue fillingShipment + } + + // Append to shipment. + shipment.AppendContainer(newSegment) + + // Set loading max wait timer on first segment. + if loadingTimer == nil { + loadingTimer = time.NewTimer(loadingMaxWaitDuration) + } + + } else if crane.stopped.IsSet() { + // If there is no new segment, this might have been triggered by a + // closed channel. Check if the crane is still active. + return nil + } + } + + sendingShipment: + for { + // Check if we are over the target load size and split the shipment. + if shipment.Length() > crane.targetLoadSize { + partialShipment, err = shipment.GetAsContainer(crane.targetLoadSize) + if err != nil { + crane.Stop(terminal.ErrInternalError.With("failed to split segment: %w", err)) + return nil + } + shipment, partialShipment = partialShipment, shipment + } + + // Load shipment. + err = crane.load(shipment) + if err != nil { + crane.Stop(terminal.ErrShipSunk.With("failed to load shipment: %w", err)) + return nil + } + + // Reset loading timer. + loadingTimer = nil + + // Continue loading with partial shipment, or a new one. + if partialShipment != nil { + // Continue loading with a partial previous shipment. + shipment, partialShipment = partialShipment, nil + + // If shipment is not big enough to send immediately, wait for more data. + if shipment.Length() < crane.targetLoadSize { + loadingTimer = time.NewTimer(loadingMaxWaitDuration) + break sendingShipment + } + + } else { + // Continue loading with new shipment. + shipment = container.New() + break sendingShipment + } + } + } +} + +func (crane *Crane) load(c *container.Container) error { + // Add Padding if needed. + if crane.opts.Padding > 0 { + paddingNeeded := int(crane.opts.Padding) - + ((c.Length() + varint.EncodedSize(uint64(c.Length()))) % int(crane.opts.Padding)) + // As the length changes slightly with the padding, we should avoid loading + // lengths around the varint size hops: + // - 128 + // - 16384 + // - 2097152 + // - 268435456 + + // Pad to target load size at maximum. + maxPadding := crane.targetLoadSize - c.Length() + if paddingNeeded > maxPadding { + paddingNeeded = maxPadding + } + + if paddingNeeded > 0 { + // Add padding indicator. + c.Append([]byte{0}) + paddingNeeded-- + + // Add needed padding data. + if paddingNeeded > 0 { + padding, err := rng.Bytes(paddingNeeded) + if err != nil { + log.Debugf("spn/docks: %s failed to get random padding data, using zeros instead", crane) + padding = make([]byte, paddingNeeded) + } + c.Append(padding) + } + } + } + + // Encrypt shipment. + c, err := crane.encrypt(c) + if err != nil { + return fmt.Errorf("failed to encrypt: %w", err) + } + + // Finalize data. + c.PrependLength() + readyToSend := c.CompileData() + + // Submit metrics. + crane.submitCraneTrafficStats(len(readyToSend)) + crane.NetState.ReportTraffic(uint64(len(readyToSend)), false) + + // Load onto ship. + err = crane.ship.Load(readyToSend) + if err != nil { + return fmt.Errorf("failed to load ship: %w", err) + } + + return nil +} + +// Stop stops the crane. +func (crane *Crane) Stop(err *terminal.Error) { + if !crane.stopped.SetToIf(false, true) { + return + } + + // Log error message. + if err != nil { + if err.IsOK() { + log.Infof("spn/docks: %s is done", crane) + } else { + log.Warningf("spn/docks: %s is stopping: %s", crane, err) + } + } + + // Unregister crane. + unregisterCrane(crane) + + // Stop all terminals. + for _, t := range crane.allTerms() { + t.Abandon(err) // Async! + } + + // Stop controller. + if crane.Controller != nil { + crane.Controller.Abandon(err) // Async! + } + + // Wait shortly for all terminals to finish abandoning. + waitStep := 50 * time.Millisecond + for i := time.Duration(0); i < maxCraneStopDuration; i += waitStep { + // Check if all terminals are done. + if crane.terminalCount() == 0 { + break + } + + time.Sleep(waitStep) + } + + // Close connection. + crane.ship.Sink() + + // Cancel crane context. + crane.cancelCtx() + + // Notify about change. + crane.NotifyUpdate() +} + +func (crane *Crane) allTerms() []terminal.Terminal { + crane.terminalsLock.Lock() + defer crane.terminalsLock.Unlock() + + terms := make([]terminal.Terminal, 0, len(crane.terminals)) + for _, term := range crane.terminals { + terms = append(terms, term) + } + + return terms +} + +func (crane *Crane) String() string { + remoteAddr := crane.ship.RemoteAddr() + switch { + case remoteAddr == nil: + return fmt.Sprintf("crane %s", crane.ID) + case crane.ship.IsMine(): + return fmt.Sprintf("crane %s to %s", crane.ID, crane.ship.MaskAddress(crane.ship.RemoteAddr())) + default: + return fmt.Sprintf("crane %s from %s", crane.ID, crane.ship.MaskAddress(crane.ship.RemoteAddr())) + } +} + +// Stopped returns whether the crane has stopped. +func (crane *Crane) Stopped() bool { + return crane.stopped.IsSet() +} diff --git a/spn/docks/crane_establish.go b/spn/docks/crane_establish.go new file mode 100644 index 00000000..3fa26732 --- /dev/null +++ b/spn/docks/crane_establish.go @@ -0,0 +1,81 @@ +package docks + +import ( + "context" + "time" + + "github.com/safing/portbase/container" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/terminal" +) + +const ( + defaultTerminalIdleTimeout = 15 * time.Minute + remoteTerminalIdleTimeout = 30 * time.Minute +) + +// EstablishNewTerminal establishes a new terminal with the crane. +func (crane *Crane) EstablishNewTerminal( + localTerm terminal.Terminal, + initData *container.Container, +) *terminal.Error { + // Create message. + msg := terminal.NewEmptyMsg() + msg.FlowID = localTerm.ID() + msg.Type = terminal.MsgTypeInit + msg.Data = initData + + // Register terminal with crane. + crane.setTerminal(localTerm) + + // Send message. + select { + case crane.controllerMsgs <- msg: + log.Debugf("spn/docks: %s initiated new terminal %d", crane, localTerm.ID()) + return nil + case <-crane.ctx.Done(): + crane.AbandonTerminal(localTerm.ID(), terminal.ErrStopping.With("initiation aborted")) + return terminal.ErrStopping + } +} + +func (crane *Crane) establishTerminal(id uint32, initData *container.Container) { + // Create new remote crane terminal. + newTerminal, _, err := NewRemoteCraneTerminal( + crane, + id, + initData, + ) + if err == nil { + // Connections via public cranes have a timeout. + if crane.Public() { + newTerminal.TerminalBase.SetTimeout(remoteTerminalIdleTimeout) + } + // Register terminal with crane. + crane.setTerminal(newTerminal) + log.Debugf("spn/docks: %s established new crane terminal %d", crane, newTerminal.ID()) + return + } + + // If something goes wrong, send an error back. + log.Warningf("spn/docks: %s failed to establish crane terminal: %s", crane, err) + + // Build abandon message. + msg := terminal.NewMsg(err.Pack()) + msg.FlowID = id + msg.Type = terminal.MsgTypeStop + + // Send message directly, or async. + select { + case crane.terminalMsgs <- msg: + default: + // Send error async. + module.StartWorker("abandon terminal", func(ctx context.Context) error { + select { + case crane.terminalMsgs <- msg: + case <-ctx.Done(): + } + return nil + }) + } +} diff --git a/spn/docks/crane_init.go b/spn/docks/crane_init.go new file mode 100644 index 00000000..740f7cdb --- /dev/null +++ b/spn/docks/crane_init.go @@ -0,0 +1,339 @@ +package docks + +import ( + "context" + "time" + + "github.com/safing/jess" + "github.com/safing/portbase/container" + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portbase/formats/varint" + "github.com/safing/portbase/info" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/terminal" +) + +/* + +Crane Init Message Format: +used by init procedures + +- Data [bytes block] + - MsgType [varint] + - Data [bytes; only when MsgType is Verify or Start*] + +Crane Init Response Format: + +- Data [bytes block] + +Crane Operational Message Format: + +- Data [bytes block] + - possibly encrypted + +*/ + +// Crane Msg Types. +const ( + CraneMsgTypeEnd = 0 + CraneMsgTypeInfo = 1 + CraneMsgTypeRequestHubInfo = 2 + CraneMsgTypeVerify = 3 + CraneMsgTypeStartEncrypted = 4 + CraneMsgTypeStartUnencrypted = 5 +) + +// Start starts the crane. +func (crane *Crane) Start(callerCtx context.Context) error { + log.Infof("spn/docks: %s is starting", crane) + + // Submit metrics. + newCranes.Inc() + + // Start crane depending on situation. + var tErr *terminal.Error + if crane.ship.IsMine() { + tErr = crane.startLocal(callerCtx) + } else { + tErr = crane.startRemote(callerCtx) + } + + // Stop crane again if starting failed. + if tErr != nil { + crane.Stop(tErr) + return tErr + } + + log.Debugf("spn/docks: %s started", crane) + // Return an explicit nil for working "!= nil" checks. + return nil +} + +func (crane *Crane) startLocal(callerCtx context.Context) *terminal.Error { + module.StartWorker("crane unloader", crane.unloader) + + if !crane.ship.IsSecure() { + // Start encrypted channel. + // Check if we have all the data we need from the Hub. + if crane.ConnectedHub == nil { + return terminal.ErrIncorrectUsage.With("cannot start encrypted channel without connected hub") + } + + // Always request hub info, as we don't know if the hub has restarted in + // the meantime and lost ephemeral keys. + hubInfoRequest := container.New( + varint.Pack8(CraneMsgTypeRequestHubInfo), + ) + hubInfoRequest.PrependLength() + err := crane.ship.Load(hubInfoRequest.CompileData()) + if err != nil { + return terminal.ErrShipSunk.With("failed to request hub info: %w", err) + } + + // Wait for reply. + var reply *container.Container + select { + case reply = <-crane.unloading: + case <-time.After(30 * time.Second): + return terminal.ErrTimeout.With("waiting for hub info") + case <-crane.ctx.Done(): + return terminal.ErrShipSunk.With("waiting for hub info") + case <-callerCtx.Done(): + return terminal.ErrCanceled.With("waiting for hub info") + } + + // Parse and import Announcement and Status. + announcementData, err := reply.GetNextBlock() + if err != nil { + return terminal.ErrMalformedData.With("failed to get announcement: %w", err) + } + statusData, err := reply.GetNextBlock() + if err != nil { + return terminal.ErrMalformedData.With("failed to get status: %w", err) + } + h, _, tErr := ImportAndVerifyHubInfo( + callerCtx, + crane.ConnectedHub.ID, + announcementData, statusData, conf.MainMapName, conf.MainMapScope, + ) + if tErr != nil { + return tErr.Wrap("failed to import and verify hub") + } + // Update reference in case it was changed by the import. + crane.ConnectedHub = h + + // Now, try to select a public key again. + signet := crane.ConnectedHub.SelectSignet() + if signet == nil { + return terminal.ErrHubNotReady.With("failed to select signet (after updating hub info)") + } + + // Configure encryption. + env := jess.NewUnconfiguredEnvelope() + env.SuiteID = jess.SuiteWireV1 + env.Recipients = []*jess.Signet{signet} + + // Do not encrypt directly, rather get session for future use, then encrypt. + crane.jession, err = env.WireCorrespondence(nil) + if err != nil { + return terminal.ErrInternalError.With("failed to create encryption session: %w", err) + } + } + + // Create crane controller. + _, initData, tErr := NewLocalCraneControllerTerminal(crane, terminal.DefaultCraneControllerOpts()) + if tErr != nil { + return tErr.Wrap("failed to set up controller") + } + + // Prepare init message for sending. + if crane.ship.IsSecure() { + initData.PrependNumber(CraneMsgTypeStartUnencrypted) + } else { + // Encrypt controller initializer. + letter, err := crane.jession.Close(initData.CompileData()) + if err != nil { + return terminal.ErrInternalError.With("failed to encrypt initial packet: %w", err) + } + initData, err = letter.ToWire() + if err != nil { + return terminal.ErrInternalError.With("failed to pack initial packet: %w", err) + } + initData.PrependNumber(CraneMsgTypeStartEncrypted) + } + + // Send start message. + initData.PrependLength() + err := crane.ship.Load(initData.CompileData()) + if err != nil { + return terminal.ErrShipSunk.With("failed to send init msg: %w", err) + } + + // Start remaining workers. + module.StartWorker("crane loader", crane.loader) + module.StartWorker("crane handler", crane.handler) + + return nil +} + +func (crane *Crane) startRemote(callerCtx context.Context) *terminal.Error { + var initMsg *container.Container + + module.StartWorker("crane unloader", crane.unloader) + +handling: + for { + // Wait for request. + var request *container.Container + select { + case request = <-crane.unloading: + + case <-time.After(30 * time.Second): + return terminal.ErrTimeout.With("waiting for crane init msg") + case <-crane.ctx.Done(): + return terminal.ErrShipSunk.With("waiting for crane init msg") + case <-callerCtx.Done(): + return terminal.ErrCanceled.With("waiting for crane init msg") + } + + msgType, err := request.GetNextN8() + if err != nil { + return terminal.ErrMalformedData.With("failed to parse crane msg type: %s", err) + } + + switch msgType { + case CraneMsgTypeEnd: + // End connection. + return terminal.ErrStopping + + case CraneMsgTypeInfo: + // Info is a terminating request. + err := crane.handleCraneInfo() + if err != nil { + return err + } + log.Debugf("spn/docks: %s sent version info", crane) + + case CraneMsgTypeRequestHubInfo: + // Handle Hub info request. + err := crane.handleCraneHubInfo() + if err != nil { + return err + } + log.Debugf("spn/docks: %s sent hub info", crane) + + case CraneMsgTypeVerify: + // Verify is a terminating request. + err := crane.handleCraneVerification(request) + if err != nil { + return err + } + log.Infof("spn/docks: %s sent hub verification", crane) + + case CraneMsgTypeStartUnencrypted: + initMsg = request + + // Start crane with initMsg. + log.Debugf("spn/docks: %s initiated unencrypted channel", crane) + break handling + + case CraneMsgTypeStartEncrypted: + if crane.identity == nil { + return terminal.ErrIncorrectUsage.With("cannot start incoming crane without designated identity") + } + + // Set up encryption. + letter, err := jess.LetterFromWire(container.New(request.CompileData())) + if err != nil { + return terminal.ErrMalformedData.With("failed to unpack initial packet: %w", err) + } + crane.jession, err = letter.WireCorrespondence(crane.identity) + if err != nil { + return terminal.ErrInternalError.With("failed to create encryption session: %w", err) + } + initMsgData, err := crane.jession.Open(letter) + if err != nil { + return terminal.ErrIntegrity.With("failed to decrypt initial packet: %w", err) + } + initMsg = container.New(initMsgData) + + // Start crane with initMsg. + log.Debugf("spn/docks: %s initiated encrypted channel", crane) + break handling + } + } + + _, _, err := NewRemoteCraneControllerTerminal(crane, initMsg) + if err != nil { + return err.Wrap("failed to start crane controller") + } + + // Start remaining workers. + module.StartWorker("crane loader", crane.loader) + module.StartWorker("crane handler", crane.handler) + + return nil +} + +func (crane *Crane) endInit() *terminal.Error { + endMsg := container.New( + varint.Pack8(CraneMsgTypeEnd), + ) + endMsg.PrependLength() + err := crane.ship.Load(endMsg.CompileData()) + if err != nil { + return terminal.ErrShipSunk.With("failed to send end msg: %w", err) + } + return nil +} + +func (crane *Crane) handleCraneInfo() *terminal.Error { + // Pack info data. + infoData, err := dsd.Dump(info.GetInfo(), dsd.JSON) + if err != nil { + return terminal.ErrInternalError.With("failed to pack info: %w", err) + } + msg := container.New(infoData) + + // Manually send reply. + msg.PrependLength() + err = crane.ship.Load(msg.CompileData()) + if err != nil { + return terminal.ErrShipSunk.With("failed to send info reply: %w", err) + } + + return nil +} + +func (crane *Crane) handleCraneHubInfo() *terminal.Error { + msg := container.New() + + // Check if we have an identity. + if crane.identity == nil { + return terminal.ErrIncorrectUsage.With("cannot handle hub info request without designated identity") + } + + // Add Hub Announcement. + announcementData, err := crane.identity.ExportAnnouncement() + if err != nil { + return terminal.ErrInternalError.With("failed to export announcement: %w", err) + } + msg.AppendAsBlock(announcementData) + + // Add Hub Status. + statusData, err := crane.identity.ExportStatus() + if err != nil { + return terminal.ErrInternalError.With("failed to export status: %w", err) + } + msg.AppendAsBlock(statusData) + + // Manually send reply. + msg.PrependLength() + err = crane.ship.Load(msg.CompileData()) + if err != nil { + return terminal.ErrShipSunk.With("failed to send hub info reply: %w", err) + } + + return nil +} diff --git a/spn/docks/crane_netstate.go b/spn/docks/crane_netstate.go new file mode 100644 index 00000000..508f5632 --- /dev/null +++ b/spn/docks/crane_netstate.go @@ -0,0 +1,131 @@ +package docks + +import ( + "sync" + "sync/atomic" + "time" +) + +// NetStatePeriodInterval defines the interval some of the net state should be reset. +const NetStatePeriodInterval = 15 * time.Minute + +// NetworkOptimizationState holds data for optimization purposes. +type NetworkOptimizationState struct { + lock sync.Mutex + + // lastSuggestedAt holds the time when the connection to the connected Hub was last suggested by the network optimization. + lastSuggestedAt time.Time + + // stoppingRequested signifies whether stopping this lane is requested. + stoppingRequested bool + // stoppingRequestedByPeer signifies whether stopping this lane is requested by the peer. + stoppingRequestedByPeer bool + // markedStoppingAt holds the time when the crane was last marked as stopping. + markedStoppingAt time.Time + + lifetimeBytesIn *uint64 + lifetimeBytesOut *uint64 + lifetimeStarted time.Time + periodBytesIn *uint64 + periodBytesOut *uint64 + periodStarted time.Time +} + +func newNetworkOptimizationState() *NetworkOptimizationState { + return &NetworkOptimizationState{ + lifetimeBytesIn: new(uint64), + lifetimeBytesOut: new(uint64), + lifetimeStarted: time.Now(), + periodBytesIn: new(uint64), + periodBytesOut: new(uint64), + periodStarted: time.Now(), + } +} + +// UpdateLastSuggestedAt sets when the lane was last suggested to the current time. +func (netState *NetworkOptimizationState) UpdateLastSuggestedAt() { + netState.lock.Lock() + defer netState.lock.Unlock() + + netState.lastSuggestedAt = time.Now() +} + +// StoppingState returns when the stopping state. +func (netState *NetworkOptimizationState) StoppingState() (requested, requestedByPeer bool, markedAt time.Time) { + netState.lock.Lock() + defer netState.lock.Unlock() + + return netState.stoppingRequested, netState.stoppingRequestedByPeer, netState.markedStoppingAt +} + +// RequestStoppingSuggested returns whether the crane should request stopping. +func (netState *NetworkOptimizationState) RequestStoppingSuggested(maxNotSuggestedDuration time.Duration) bool { + netState.lock.Lock() + defer netState.lock.Unlock() + + return time.Now().Add(-maxNotSuggestedDuration).After(netState.lastSuggestedAt) +} + +// StoppingSuggested returns whether the crane should be marked as stopping. +func (netState *NetworkOptimizationState) StoppingSuggested() bool { + netState.lock.Lock() + defer netState.lock.Unlock() + + return netState.stoppingRequested && + netState.stoppingRequestedByPeer +} + +// StopSuggested returns whether the crane should be stopped. +func (netState *NetworkOptimizationState) StopSuggested() bool { + netState.lock.Lock() + defer netState.lock.Unlock() + + return netState.stoppingRequested && + netState.stoppingRequestedByPeer && + !netState.markedStoppingAt.IsZero() && + time.Now().Add(-maxCraneStoppingDuration).After(netState.markedStoppingAt) +} + +// ReportTraffic adds the reported transferred data to the traffic stats. +func (netState *NetworkOptimizationState) ReportTraffic(bytes uint64, in bool) { + if in { + atomic.AddUint64(netState.lifetimeBytesIn, bytes) + atomic.AddUint64(netState.periodBytesIn, bytes) + } else { + atomic.AddUint64(netState.lifetimeBytesOut, bytes) + atomic.AddUint64(netState.periodBytesOut, bytes) + } +} + +// LapsePeriod lapses the net state period, if needed. +func (netState *NetworkOptimizationState) LapsePeriod() { + netState.lock.Lock() + defer netState.lock.Unlock() + + // Reset period if interval elapsed. + if time.Now().Add(-NetStatePeriodInterval).After(netState.periodStarted) { + atomic.StoreUint64(netState.periodBytesIn, 0) + atomic.StoreUint64(netState.periodBytesOut, 0) + netState.periodStarted = time.Now() + } +} + +// GetTrafficStats returns the traffic stats. +func (netState *NetworkOptimizationState) GetTrafficStats() ( + lifetimeBytesIn uint64, + lifetimeBytesOut uint64, + lifetimeStarted time.Time, + periodBytesIn uint64, + periodBytesOut uint64, + periodStarted time.Time, +) { + netState.lock.Lock() + defer netState.lock.Unlock() + + return atomic.LoadUint64(netState.lifetimeBytesIn), + atomic.LoadUint64(netState.lifetimeBytesOut), + netState.lifetimeStarted, + atomic.LoadUint64(netState.periodBytesIn), + atomic.LoadUint64(netState.periodBytesOut), + netState.periodStarted +} diff --git a/spn/docks/crane_terminal.go b/spn/docks/crane_terminal.go new file mode 100644 index 00000000..731bf953 --- /dev/null +++ b/spn/docks/crane_terminal.go @@ -0,0 +1,122 @@ +package docks + +import ( + "net" + + "github.com/safing/portbase/container" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/terminal" +) + +// CraneTerminal is a terminal started by a crane. +type CraneTerminal struct { + *terminal.TerminalBase + + // Add-Ons + terminal.SessionAddOn + + crane *Crane +} + +// NewLocalCraneTerminal returns a new local crane terminal. +func NewLocalCraneTerminal( + crane *Crane, + remoteHub *hub.Hub, + initMsg *terminal.TerminalOpts, +) (*CraneTerminal, *container.Container, *terminal.Error) { + // Create Terminal Base. + t, initData, err := terminal.NewLocalBaseTerminal( + crane.ctx, + crane.getNextTerminalID(), + crane.ID, + remoteHub, + initMsg, + crane, + ) + if err != nil { + return nil, nil, err + } + + return initCraneTerminal(crane, t), initData, nil +} + +// NewRemoteCraneTerminal returns a new remote crane terminal. +func NewRemoteCraneTerminal( + crane *Crane, + id uint32, + initData *container.Container, +) (*CraneTerminal, *terminal.TerminalOpts, *terminal.Error) { + // Create Terminal Base. + t, initMsg, err := terminal.NewRemoteBaseTerminal( + crane.ctx, + id, + crane.ID, + crane.identity, + initData, + crane, + ) + if err != nil { + return nil, nil, err + } + + return initCraneTerminal(crane, t), initMsg, nil +} + +func initCraneTerminal( + crane *Crane, + t *terminal.TerminalBase, +) *CraneTerminal { + // Create Crane Terminal and assign it as the extended Terminal. + ct := &CraneTerminal{ + TerminalBase: t, + crane: crane, + } + t.SetTerminalExtension(ct) + + // Start workers. + t.StartWorkers(module, "crane terminal") + + return ct +} + +// GrantPermission grants the given permissions. +// Additionally, it will mark the crane as authenticated, if not public. +func (t *CraneTerminal) GrantPermission(grant terminal.Permission) { + // Forward granted permission to base terminal. + t.TerminalBase.GrantPermission(grant) + + // Mark crane as authenticated if not public or already authenticated. + if !t.crane.Public() && !t.crane.Authenticated() { + t.crane.authenticated.Set() + + // Submit metrics. + newAuthenticatedCranes.Inc() + } +} + +// LocalAddr returns the crane's local address. +func (t *CraneTerminal) LocalAddr() net.Addr { + return t.crane.LocalAddr() +} + +// RemoteAddr returns the crane's remote address. +func (t *CraneTerminal) RemoteAddr() net.Addr { + return t.crane.RemoteAddr() +} + +// Transport returns the crane's transport. +func (t *CraneTerminal) Transport() *hub.Transport { + return t.crane.Transport() +} + +// IsBeingAbandoned returns whether the terminal is being abandoned. +func (t *CraneTerminal) IsBeingAbandoned() bool { + return t.Abandoning.IsSet() +} + +// HandleDestruction gives the terminal the ability to clean up. +// The terminal has already fully shut down at this point. +// Should never be called directly. Call Abandon() instead. +func (t *CraneTerminal) HandleDestruction(err *terminal.Error) { + t.crane.AbandonTerminal(t.ID(), err) +} diff --git a/spn/docks/crane_test.go b/spn/docks/crane_test.go new file mode 100644 index 00000000..9e13b5e1 --- /dev/null +++ b/spn/docks/crane_test.go @@ -0,0 +1,267 @@ +package docks + +import ( + "context" + "fmt" + "os" + "runtime/pprof" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/safing/portmaster/spn/cabin" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/ships" + "github.com/safing/portmaster/spn/terminal" +) + +func TestCraneCommunication(t *testing.T) { + t.Parallel() + + testCraneWithCounter(t, "plain-counter-load-100", false, 100, 1000) + testCraneWithCounter(t, "plain-counter-load-1000", false, 1000, 1000) + testCraneWithCounter(t, "plain-counter-load-10000", false, 10000, 1000) + testCraneWithCounter(t, "encrypted-counter", true, 1000, 1000) +} + +func testCraneWithCounter(t *testing.T, testID string, encrypting bool, loadSize int, countTo uint64) { //nolint:unparam,thelper + var identity *cabin.Identity + var connectedHub *hub.Hub + if encrypting { + identity, connectedHub = getTestIdentity(t) + } + + // Build ship and cranes. + optimalMinLoadSize = loadSize * 2 + ship := ships.NewTestShip(!encrypting, loadSize) + + var crane1, crane2 *Crane + var craneWg sync.WaitGroup + craneWg.Add(2) + + go func() { + var err error + crane1, err = NewCrane(ship, connectedHub, nil) + if err != nil { + panic(fmt.Sprintf("crane test %s could not create crane1: %s", testID, err)) + } + err = crane1.Start(module.Ctx) + if err != nil { + panic(fmt.Sprintf("crane test %s could not start crane1: %s", testID, err)) + } + craneWg.Done() + }() + go func() { + var err error + crane2, err = NewCrane(ship.Reverse(), nil, identity) + if err != nil { + panic(fmt.Sprintf("crane test %s could not create crane2: %s", testID, err)) + } + err = crane2.Start(module.Ctx) + if err != nil { + panic(fmt.Sprintf("crane test %s could not start crane2: %s", testID, err)) + } + craneWg.Done() + }() + + craneWg.Wait() + t.Logf("crane test %s setup complete", testID) + + // Wait async for test to complete, print stack after timeout. + finished := make(chan struct{}) + go func() { + select { + case <-finished: + case <-time.After(10 * time.Second): + t.Logf("crane test %s is taking too long, print stack:", testID) + _ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) + os.Exit(1) + } + }() + + t.Logf("crane1 controller: %+v", crane1.Controller) + t.Logf("crane2 controller: %+v", crane2.Controller) + + // Start counters for testing. + op1, tErr := terminal.NewCounterOp(crane1.Controller, terminal.CounterOpts{ + ClientCountTo: countTo, + ServerCountTo: countTo, + }) + if tErr != nil { + t.Fatalf("crane test %s failed to run counter op: %s", testID, tErr) + } + + // Wait for completion. + op1.Wait() + close(finished) + + // Wait a little so that all errors can be propagated, so we can truly see + // if we succeeded. + time.Sleep(1 * time.Second) + + // Check errors. + if op1.Error != nil { + t.Fatalf("crane test %s counter op1 failed: %s", testID, op1.Error) + } +} + +type StreamingTerminal struct { + terminal.BareTerminal + + test *testing.T + id uint32 + crane *Crane + recv chan *terminal.Msg + testData []byte +} + +func (t *StreamingTerminal) ID() uint32 { + return t.id +} + +func (t *StreamingTerminal) Ctx() context.Context { + return module.Ctx +} + +func (t *StreamingTerminal) Deliver(msg *terminal.Msg) *terminal.Error { + t.recv <- msg + msg.Finish() + return nil +} + +func (t *StreamingTerminal) Abandon(err *terminal.Error) { + t.crane.AbandonTerminal(t.ID(), err) + if err != nil { + t.test.Errorf("streaming terminal %d failed: %s", t.id, err) + } +} + +func (t *StreamingTerminal) FmtID() string { + return fmt.Sprintf("test-%d", t.id) +} + +func TestCraneLoadingUnloading(t *testing.T) { + t.Parallel() + + testCraneWithStreaming(t, "plain-streaming", false, 100) + testCraneWithStreaming(t, "encrypted-streaming", true, 100) +} + +func testCraneWithStreaming(t *testing.T, testID string, encrypting bool, loadSize int) { //nolint:thelper + var identity *cabin.Identity + var connectedHub *hub.Hub + if encrypting { + identity, connectedHub = getTestIdentity(t) + } + + // Build ship and cranes. + optimalMinLoadSize = loadSize * 2 + ship := ships.NewTestShip(!encrypting, loadSize) + + var crane1, crane2 *Crane + var craneWg sync.WaitGroup + craneWg.Add(2) + + go func() { + var err error + crane1, err = NewCrane(ship, connectedHub, nil) + if err != nil { + panic(fmt.Sprintf("crane test %s could not create crane1: %s", testID, err)) + } + err = crane1.Start(module.Ctx) + if err != nil { + panic(fmt.Sprintf("crane test %s could not start crane1: %s", testID, err)) + } + craneWg.Done() + }() + go func() { + var err error + crane2, err = NewCrane(ship.Reverse(), nil, identity) + if err != nil { + panic(fmt.Sprintf("crane test %s could not create crane2: %s", testID, err)) + } + err = crane2.Start(module.Ctx) + if err != nil { + panic(fmt.Sprintf("crane test %s could not start crane2: %s", testID, err)) + } + craneWg.Done() + }() + + craneWg.Wait() + t.Logf("crane test %s setup complete", testID) + + // Wait async for test to complete, print stack after timeout. + finished := make(chan struct{}) + go func() { + select { + case <-finished: + case <-time.After(10 * time.Second): + t.Logf("crane test %s is taking too long, print stack:", testID) + _ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) + os.Exit(1) + } + }() + + t.Logf("crane1 controller: %+v", crane1.Controller) + t.Logf("crane2 controller: %+v", crane2.Controller) + + // Create terminals and run test. + st := &StreamingTerminal{ + test: t, + id: 8, + crane: crane2, + recv: make(chan *terminal.Msg), + testData: []byte("The quick brown fox jumps over the lazy dog."), + } + crane2.terminals[st.ID()] = st + + // Run streaming test. + var streamingWg sync.WaitGroup + streamingWg.Add(2) + count := 10000 + go func() { + for i := 1; i <= count; i++ { + msg := terminal.NewMsg(st.testData) + msg.FlowID = st.id + err := crane1.Send(msg, 1*time.Second) + if err != nil { + msg.Finish() + crane1.Stop(err.Wrap("failed to submit terminal msg")) + } + // log.Tracef("spn/testing: + %d", i) + } + t.Logf("crane test %s done with sending", testID) + streamingWg.Done() + }() + go func() { + for i := 1; i <= count; i++ { + msg := <-st.recv + assert.Equal(t, st.testData, msg.Data.CompileData(), "data mismatched") + // log.Tracef("spn/testing: - %d", i) + } + t.Logf("crane test %s done with receiving", testID) + streamingWg.Done() + }() + + // Wait for completion. + streamingWg.Wait() + close(finished) +} + +var testIdentity *cabin.Identity + +func getTestIdentity(t *testing.T) (*cabin.Identity, *hub.Hub) { + t.Helper() + + if testIdentity == nil { + var err error + testIdentity, err = cabin.CreateIdentity(module.Ctx, "test") + if err != nil { + t.Fatalf("failed to create identity: %s", err) + } + } + + return testIdentity, testIdentity.Hub +} diff --git a/spn/docks/crane_verify.go b/spn/docks/crane_verify.go new file mode 100644 index 00000000..1f4e686d --- /dev/null +++ b/spn/docks/crane_verify.go @@ -0,0 +1,85 @@ +package docks + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/safing/portbase/container" + "github.com/safing/portbase/formats/varint" + "github.com/safing/portmaster/spn/cabin" + "github.com/safing/portmaster/spn/terminal" +) + +const ( + hubVerificationPurpose = "hub identify verification" +) + +// VerifyConnectedHub verifies the connected Hub. +func (crane *Crane) VerifyConnectedHub(callerCtx context.Context) error { + if !crane.ship.IsMine() || crane.nextTerminalID != 0 || crane.Public() { + return errors.New("hub verification can only be executed in init phase by the client") + } + + // Create verification request. + v, request, err := cabin.CreateVerificationRequest(hubVerificationPurpose, "", "") + if err != nil { + return fmt.Errorf("failed to create verification request: %w", err) + } + + // Send it. + msg := container.New( + varint.Pack8(CraneMsgTypeVerify), + request, + ) + msg.PrependLength() + err = crane.ship.Load(msg.CompileData()) + if err != nil { + return terminal.ErrShipSunk.With("failed to send verification request: %w", err) + } + + // Wait for reply. + var reply *container.Container + select { + case reply = <-crane.unloading: + case <-time.After(2 * time.Minute): + // Use a big timeout here, as this might keep servers from joining the + // network at all, as every servers needs to verify every server, no + // matter how far away. + return terminal.ErrTimeout.With("waiting for verification reply") + case <-crane.ctx.Done(): + return terminal.ErrShipSunk.With("waiting for verification reply") + case <-callerCtx.Done(): + return terminal.ErrShipSunk.With("waiting for verification reply") + } + + // Verify reply. + return v.Verify(reply.CompileData(), crane.ConnectedHub) +} + +func (crane *Crane) handleCraneVerification(request *container.Container) *terminal.Error { + // Check if we have an identity. + if crane.identity == nil { + return terminal.ErrIncorrectUsage.With("cannot handle verification request without designated identity") + } + + response, err := crane.identity.SignVerificationRequest( + request.CompileData(), + hubVerificationPurpose, + "", "", + ) + if err != nil { + return terminal.ErrInternalError.With("failed to sign verification request: %w", err) + } + msg := container.New(response) + + // Manually send reply. + msg.PrependLength() + err = crane.ship.Load(msg.CompileData()) + if err != nil { + return terminal.ErrShipSunk.With("failed to send verification reply: %w", err) + } + + return nil +} diff --git a/spn/docks/cranehooks.go b/spn/docks/cranehooks.go new file mode 100644 index 00000000..0355a4f7 --- /dev/null +++ b/spn/docks/cranehooks.go @@ -0,0 +1,46 @@ +package docks + +import ( + "sync" + + "github.com/safing/portbase/log" +) + +var ( + craneUpdateHook func(crane *Crane) + craneUpdateHookLock sync.Mutex +) + +// RegisterCraneUpdateHook allows the captain to hook into receiving updates for cranes. +func RegisterCraneUpdateHook(fn func(crane *Crane)) { + craneUpdateHookLock.Lock() + defer craneUpdateHookLock.Unlock() + + if craneUpdateHook == nil { + craneUpdateHook = fn + } else { + log.Error("spn/docks: crane update hook already registered") + } +} + +// ResetCraneUpdateHook resets the hook for receiving updates for cranes. +func ResetCraneUpdateHook() { + craneUpdateHookLock.Lock() + defer craneUpdateHookLock.Unlock() + + craneUpdateHook = nil +} + +// NotifyUpdate calls the registers crane update hook function. +func (crane *Crane) NotifyUpdate() { + if crane == nil { + return + } + + craneUpdateHookLock.Lock() + defer craneUpdateHookLock.Unlock() + + if craneUpdateHook != nil { + craneUpdateHook(crane) + } +} diff --git a/spn/docks/hub_import.go b/spn/docks/hub_import.go new file mode 100644 index 00000000..377164f2 --- /dev/null +++ b/spn/docks/hub_import.go @@ -0,0 +1,189 @@ +package docks + +import ( + "context" + "fmt" + "net" + "sync" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/ships" + "github.com/safing/portmaster/spn/terminal" +) + +var hubImportLock sync.Mutex + +// ImportAndVerifyHubInfo imports the given hub message and verifies them. +func ImportAndVerifyHubInfo(ctx context.Context, hubID string, announcementData, statusData []byte, mapName string, scope hub.Scope) (h *hub.Hub, forward bool, tErr *terminal.Error) { + var firstErr *terminal.Error + + // Synchronize import, as we might easily learn of a new hub from different + // gossip channels simultaneously. + hubImportLock.Lock() + defer hubImportLock.Unlock() + + // Check arguments. + if announcementData == nil && statusData == nil { + return nil, false, terminal.ErrInternalError.With("no announcement or status supplied") + } + + // Import Announcement, if given. + var hubKnown, hubChanged bool + if announcementData != nil { + hubFromMsg, known, changed, err := hub.ApplyAnnouncement(nil, announcementData, mapName, scope, false) + if err != nil && firstErr == nil { + firstErr = terminal.ErrInternalError.With("failed to apply announcement: %w", err) + } + if known { + hubKnown = true + } + if changed { + hubChanged = true + } + if hubFromMsg != nil { + h = hubFromMsg + } + } + + // Import Status, if given. + if statusData != nil { + hubFromMsg, known, changed, err := hub.ApplyStatus(h, statusData, mapName, scope, false) + if err != nil && firstErr == nil { + firstErr = terminal.ErrInternalError.With("failed to apply status: %w", err) + } + if known && announcementData == nil { + // If we parsed an announcement before, "known" will always be true here, + // as we supply hub.ApplyStatus with a hub. + hubKnown = true + } + if changed { + hubChanged = true + } + if hubFromMsg != nil { + h = hubFromMsg + } + } + + // Only continue if we now have a Hub. + if h == nil { + if firstErr != nil { + return nil, false, firstErr + } + return nil, false, terminal.ErrInternalError.With("got not hub after data import") + } + + // Abort if the given hub ID does not match. + // We may have just connected to the wrong IP address. + if hubID != "" && h.ID != hubID { + return nil, false, terminal.ErrInternalError.With("hub mismatch") + } + + // Verify hub if: + // - There is no error up until here. + // - There has been any change. + // - The hub is not verified yet. + // - We're a public Hub. + // - We're not testing. + if firstErr == nil && hubChanged && !h.Verified() && conf.PublicHub() && !runningTests { + if !conf.HubHasIPv4() && !conf.HubHasIPv6() { + firstErr = terminal.ErrInternalError.With("no hub networks set") + } + if h.Info.IPv4 != nil && conf.HubHasIPv4() { + err := verifyHubIP(ctx, h, h.Info.IPv4) + if err != nil { + firstErr = terminal.ErrIntegrity.With("failed to verify IPv4 address %s of %s: %w", h.Info.IPv4, h, err) + } + } + if h.Info.IPv6 != nil && conf.HubHasIPv6() { + err := verifyHubIP(ctx, h, h.Info.IPv6) + if err != nil { + firstErr = terminal.ErrIntegrity.With("failed to verify IPv6 address %s of %s: %w", h.Info.IPv6, h, err) + } + } + + if firstErr != nil { + func() { + h.Lock() + defer h.Unlock() + h.InvalidInfo = true + }() + log.Warningf("spn/docks: failed to verify IPs of %s: %s", h, firstErr) + } else { + func() { + h.Lock() + defer h.Unlock() + h.VerifiedIPs = true + }() + log.Infof("spn/docks: verified IPs of %s: IPv4=%s IPv6=%s", h, h.Info.IPv4, h.Info.IPv6) + } + } + + // Dismiss initial imports with errors. + if !hubKnown && firstErr != nil { + return nil, false, firstErr + } + + // Don't do anything if nothing changed. + if !hubChanged { + return h, false, firstErr + } + + // We now have one of: + // - A unknown Hub without error. + // - A known Hub without error. + // - A known Hub with error, which we want to save and propagate. + + // Save the Hub to the database. + err := h.Save() + if err != nil { + log.Errorf("spn/docks: failed to persist %s: %s", h, err) + } + + // Save the raw messages to the database. + if announcementData != nil { + err = hub.SaveHubMsg(h.ID, h.Map, hub.MsgTypeAnnouncement, announcementData) + if err != nil { + log.Errorf("spn/docks: failed to save raw announcement msg of %s: %s", h, err) + } + } + if statusData != nil { + err = hub.SaveHubMsg(h.ID, h.Map, hub.MsgTypeStatus, statusData) + if err != nil { + log.Errorf("spn/docks: failed to save raw status msg of %s: %s", h, err) + } + } + + return h, true, firstErr +} + +func verifyHubIP(ctx context.Context, h *hub.Hub, ip net.IP) error { + // Create connection. + ship, err := ships.Launch(ctx, h, nil, ip) + if err != nil { + return fmt.Errorf("failed to launch ship to %s: %w", ip, err) + } + + // Start crane for receiving reply. + crane, err := NewCrane(ship, h, nil) + if err != nil { + return fmt.Errorf("failed to create crane: %w", err) + } + module.StartWorker("crane unloader", crane.unloader) + defer crane.Stop(nil) + + // Verify Hub. + err = crane.VerifyConnectedHub(ctx) + if err != nil { + return err + } + + // End connection. + tErr := crane.endInit() + if tErr != nil { + log.Debugf("spn/docks: failed to end verification connection to %s: %s", ip, tErr) + } + + return nil +} diff --git a/spn/docks/measurements.go b/spn/docks/measurements.go new file mode 100644 index 00000000..ed2edfb3 --- /dev/null +++ b/spn/docks/measurements.go @@ -0,0 +1,108 @@ +package docks + +import ( + "context" + "fmt" + "time" + + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/ships" + "github.com/safing/portmaster/spn/terminal" +) + +// Measurement Configuration. +const ( + CraneMeasurementTTLDefault = 30 * time.Minute + CraneMeasurementTTLByCostBase = 1 * time.Minute + CraneMeasurementTTLByCostMin = 30 * time.Minute + CraneMeasurementTTLByCostMax = 3 * time.Hour + + // With a base TTL of 1m, this leads to: + // 20c -> 20m -> raised to 30m + // 50c -> 50m + // 100c -> 1h40m + // 1000c -> 16h40m -> capped to 3h. +) + +// MeasureHub measures the connection to this Hub and saves the results to the +// Hub. +func MeasureHub(ctx context.Context, h *hub.Hub, checkExpiryWith time.Duration) *terminal.Error { + // Check if we are measuring before building a connection. + if capacityTestRunning.IsSet() { + return terminal.ErrTryAgainLater.With("another capacity op is already running") + } + + // Check if we have a connection to this Hub. + crane := GetAssignedCrane(h.ID) + if crane == nil { + // Connect to Hub. + var err error + crane, err = establishCraneForMeasuring(ctx, h) + if err != nil { + return terminal.ErrConnectionError.With("failed to connect to %s: %s", h, err) + } + // Stop crane if established just for measuring. + defer crane.Stop(nil) + } + + // Run latency test. + _, expires := h.GetMeasurements().GetLatency() + if checkExpiryWith == 0 || time.Now().Add(-checkExpiryWith).After(expires) { + latOp, tErr := NewLatencyTestOp(crane.Controller) + if !tErr.IsOK() { + return tErr + } + select { + case tErr = <-latOp.Result(): + if !tErr.IsOK() { + return tErr + } + case <-ctx.Done(): + return terminal.ErrCanceled + case <-time.After(1 * time.Minute): + crane.Controller.StopOperation(latOp, terminal.ErrTimeout) + return terminal.ErrTimeout.With("waiting for latency test") + } + } + + // Run capacity test. + _, expires = h.GetMeasurements().GetCapacity() + if checkExpiryWith == 0 || time.Now().Add(-checkExpiryWith).After(expires) { + capOp, tErr := NewCapacityTestOp(crane.Controller, nil) + if !tErr.IsOK() { + return tErr + } + select { + case tErr = <-capOp.Result(): + if !tErr.IsOK() { + return tErr + } + case <-ctx.Done(): + return terminal.ErrCanceled + case <-time.After(1 * time.Minute): + crane.Controller.StopOperation(capOp, terminal.ErrTimeout) + return terminal.ErrTimeout.With("waiting for capacity test") + } + } + + return nil +} + +func establishCraneForMeasuring(ctx context.Context, dst *hub.Hub) (*Crane, error) { + ship, err := ships.Launch(ctx, dst, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to launch ship: %w", err) + } + + crane, err := NewCrane(ship, dst, nil) + if err != nil { + return nil, fmt.Errorf("failed to create crane: %w", err) + } + + err = crane.Start(ctx) + if err != nil { + return nil, fmt.Errorf("failed to start crane: %w", err) + } + + return crane, nil +} diff --git a/spn/docks/metrics.go b/spn/docks/metrics.go new file mode 100644 index 00000000..49df92bd --- /dev/null +++ b/spn/docks/metrics.go @@ -0,0 +1,404 @@ +package docks + +import ( + "sync" + "sync/atomic" + "time" + + "github.com/tevino/abool" + + "github.com/safing/portbase/api" + "github.com/safing/portbase/metrics" +) + +var ( + newCranes *metrics.Counter + newPublicCranes *metrics.Counter + newAuthenticatedCranes *metrics.Counter + + trafficBytesPublicCranes *metrics.Counter + trafficBytesAuthenticatedCranes *metrics.Counter + trafficBytesPrivateCranes *metrics.Counter + + newExpandOp *metrics.Counter + expandOpDurationHistogram *metrics.Histogram + expandOpRelayedDataHistogram *metrics.Histogram + + metricsRegistered = abool.New() +) + +func registerMetrics() (err error) { + // Only register metrics once. + if !metricsRegistered.SetToIf(false, true) { + return nil + } + + // Total Crane Stats. + + newCranes, err = metrics.NewCounter( + "spn/cranes/total", + nil, + &metrics.Options{ + Name: "SPN New Cranes", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + newPublicCranes, err = metrics.NewCounter( + "spn/cranes/public/total", + nil, + &metrics.Options{ + Name: "SPN New Public Cranes", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + newAuthenticatedCranes, err = metrics.NewCounter( + "spn/cranes/authenticated/total", + nil, + &metrics.Options{ + Name: "SPN New Authenticated Cranes", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + // Active Crane Stats. + + _, err = metrics.NewGauge( + "spn/cranes/active", + map[string]string{ + "status": "public", + }, + getActivePublicCranes, + &metrics.Options{ + Name: "SPN Active Public Cranes", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + _, err = metrics.NewGauge( + "spn/cranes/active", + map[string]string{ + "status": "authenticated", + }, + getActiveAuthenticatedCranes, + &metrics.Options{ + Name: "SPN Active Authenticated Cranes", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + _, err = metrics.NewGauge( + "spn/cranes/active", + map[string]string{ + "status": "private", + }, + getActivePrivateCranes, + &metrics.Options{ + Name: "SPN Active Private Cranes", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + _, err = metrics.NewGauge( + "spn/cranes/active", + map[string]string{ + "status": "stopping", + }, + getActiveStoppingCranes, + &metrics.Options{ + Name: "SPN Active Stopping Cranes", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + // Crane Traffic Stats. + + trafficBytesPublicCranes, err = metrics.NewCounter( + "spn/cranes/bytes", + map[string]string{ + "status": "public", + }, + &metrics.Options{ + Name: "SPN Public Crane Traffic", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + trafficBytesAuthenticatedCranes, err = metrics.NewCounter( + "spn/cranes/bytes", + map[string]string{ + "status": "authenticated", + }, + &metrics.Options{ + Name: "SPN Authenticated Crane Traffic", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + trafficBytesPrivateCranes, err = metrics.NewCounter( + "spn/cranes/bytes", + map[string]string{ + "status": "private", + }, + &metrics.Options{ + Name: "SPN Private Crane Traffic", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + // Lane Stats. + + _, err = metrics.NewGauge( + "spn/lanes/latency/avg/seconds", + nil, + getAvgLaneLatencyStat, + &metrics.Options{ + Name: "SPN Avg Lane Latency", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + _, err = metrics.NewGauge( + "spn/lanes/latency/min/seconds", + nil, + getMinLaneLatencyStat, + &metrics.Options{ + Name: "SPN Min Lane Latency", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + _, err = metrics.NewGauge( + "spn/lanes/capacity/avg/bytes", + nil, + getAvgLaneCapacityStat, + &metrics.Options{ + Name: "SPN Avg Lane Capacity", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + _, err = metrics.NewGauge( + "spn/lanes/capacity/max/bytes", + nil, + getMaxLaneCapacityStat, + &metrics.Options{ + Name: "SPN Max Lane Capacity", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + // Expand Op Stats. + + newExpandOp, err = metrics.NewCounter( + "spn/op/expand/total", + nil, + &metrics.Options{ + Name: "SPN Total Expand Operations", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + _, err = metrics.NewGauge( + "spn/op/expand/active", + nil, + getActiveExpandOpsStat, + &metrics.Options{ + Name: "SPN Active Expand Operations", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + expandOpDurationHistogram, err = metrics.NewHistogram( + "spn/op/expand/histogram/duration/seconds", + nil, + &metrics.Options{ + Name: "SPN Expand Operation Duration Histogram", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + expandOpRelayedDataHistogram, err = metrics.NewHistogram( + "spn/op/expand/histogram/traffic/bytes", + nil, + &metrics.Options{ + Name: "SPN Expand Operation Relayed Data Histogram", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + return err +} + +func getActiveExpandOpsStat() float64 { + return float64(atomic.LoadInt64(activeExpandOps)) +} + +var ( + craneStats *craneGauges + craneStatsExpires time.Time + craneStatsLock sync.Mutex + craneStatsTTL = 55 * time.Second +) + +type craneGauges struct { + publicActive float64 + authenticatedActive float64 + privateActive float64 + stoppingActive float64 + + laneLatencyAvg float64 + laneLatencyMin float64 + laneCapacityAvg float64 + laneCapacityMax float64 +} + +func getActivePublicCranes() float64 { return getCraneStats().publicActive } +func getActiveAuthenticatedCranes() float64 { return getCraneStats().authenticatedActive } +func getActivePrivateCranes() float64 { return getCraneStats().privateActive } +func getActiveStoppingCranes() float64 { return getCraneStats().stoppingActive } +func getAvgLaneLatencyStat() float64 { return getCraneStats().laneLatencyAvg } +func getMinLaneLatencyStat() float64 { return getCraneStats().laneLatencyMin } +func getAvgLaneCapacityStat() float64 { return getCraneStats().laneCapacityAvg } +func getMaxLaneCapacityStat() float64 { return getCraneStats().laneCapacityMax } + +func getCraneStats() *craneGauges { + craneStatsLock.Lock() + defer craneStatsLock.Unlock() + + // Return cache if still valid. + if time.Now().Before(craneStatsExpires) { + return craneStats + } + + // Refresh. + craneStats = &craneGauges{} + var laneStatCnt float64 + for _, crane := range getAllCranes() { + switch { + case crane.Stopped(): + continue + case crane.IsStopping(): + craneStats.stoppingActive++ + continue + case crane.Public(): + craneStats.publicActive++ + case crane.Authenticated(): + craneStats.authenticatedActive++ + continue + default: + craneStats.privateActive++ + continue + } + + // Get lane stats. + if crane.ConnectedHub == nil { + continue + } + measurements := crane.ConnectedHub.GetMeasurements() + laneLatency, _ := measurements.GetLatency() + if laneLatency == 0 { + continue + } + laneCapacity, _ := measurements.GetCapacity() + if laneCapacity == 0 { + continue + } + + // Only use data if both latency and capacity is available. + laneStatCnt++ + + // Convert to base unit: seconds. + latency := laneLatency.Seconds() + // Add to avg and set min if lower. + craneStats.laneLatencyAvg += latency + if craneStats.laneLatencyMin > latency || craneStats.laneLatencyMin == 0 { + craneStats.laneLatencyMin = latency + } + + // Convert in base unit: bytes. + capacity := float64(laneCapacity) / 8 + // Add to avg and set max if higher. + craneStats.laneCapacityAvg += capacity + if craneStats.laneCapacityMax < capacity { + craneStats.laneCapacityMax = capacity + } + } + + // Create averages. + if laneStatCnt > 0 { + craneStats.laneLatencyAvg /= laneStatCnt + craneStats.laneCapacityAvg /= laneStatCnt + } + + craneStatsExpires = time.Now().Add(craneStatsTTL) + return craneStats +} + +func (crane *Crane) submitCraneTrafficStats(bytes int) { + switch { + case crane.Stopped(): + return + case crane.Public(): + trafficBytesPublicCranes.Add(bytes) + case crane.Authenticated(): + trafficBytesAuthenticatedCranes.Add(bytes) + default: + trafficBytesPrivateCranes.Add(bytes) + } +} diff --git a/spn/docks/module.go b/spn/docks/module.go new file mode 100644 index 00000000..31a4da95 --- /dev/null +++ b/spn/docks/module.go @@ -0,0 +1,117 @@ +package docks + +import ( + "encoding/hex" + "errors" + "fmt" + "sync" + + "github.com/safing/portbase/modules" + "github.com/safing/portbase/rng" + _ "github.com/safing/portmaster/spn/access" +) + +var ( + module *modules.Module + + allCranes = make(map[string]*Crane) // ID = Crane ID + assignedCranes = make(map[string]*Crane) // ID = connected Hub ID + cranesLock sync.RWMutex + + runningTests bool +) + +func init() { + module = modules.Register("docks", nil, start, stopAllCranes, "terminal", "cabin", "access") +} + +func start() error { + return registerMetrics() +} + +func registerCrane(crane *Crane) error { + cranesLock.Lock() + defer cranesLock.Unlock() + + // Generate new IDs until a unique one is found. + for i := 0; i < 100; i++ { + // Generate random ID. + randomID, err := rng.Bytes(3) + if err != nil { + return fmt.Errorf("failed to generate crane ID: %w", err) + } + newID := hex.EncodeToString(randomID) + + // Check if ID already exists. + _, ok := allCranes[newID] + if !ok { + crane.ID = newID + allCranes[crane.ID] = crane + return nil + } + } + + return errors.New("failed to find unique crane ID") +} + +func unregisterCrane(crane *Crane) { + cranesLock.Lock() + defer cranesLock.Unlock() + + delete(allCranes, crane.ID) + if crane.ConnectedHub != nil { + delete(assignedCranes, crane.ConnectedHub.ID) + } +} + +func stopAllCranes() error { + for _, crane := range getAllCranes() { + crane.Stop(nil) + } + return nil +} + +// AssignCrane assigns a crane to the given Hub ID. +func AssignCrane(hubID string, crane *Crane) { + cranesLock.Lock() + defer cranesLock.Unlock() + + assignedCranes[hubID] = crane +} + +// GetAssignedCrane returns the assigned crane of the given Hub ID. +func GetAssignedCrane(hubID string) *Crane { + cranesLock.RLock() + defer cranesLock.RUnlock() + + crane, ok := assignedCranes[hubID] + if ok { + return crane + } + return nil +} + +func getAllCranes() map[string]*Crane { + copiedCranes := make(map[string]*Crane, len(allCranes)) + + cranesLock.RLock() + defer cranesLock.RUnlock() + + for id, crane := range allCranes { + copiedCranes[id] = crane + } + return copiedCranes +} + +// GetAllAssignedCranes returns a copy of the map of all assigned cranes. +func GetAllAssignedCranes() map[string]*Crane { + copiedCranes := make(map[string]*Crane, len(assignedCranes)) + + cranesLock.RLock() + defer cranesLock.RUnlock() + + for destination, crane := range assignedCranes { + copiedCranes[destination] = crane + } + return copiedCranes +} diff --git a/spn/docks/module_test.go b/spn/docks/module_test.go new file mode 100644 index 00000000..0383cc21 --- /dev/null +++ b/spn/docks/module_test.go @@ -0,0 +1,16 @@ +package docks + +import ( + "testing" + + "github.com/safing/portmaster/service/core/pmtesting" + "github.com/safing/portmaster/spn/access" + "github.com/safing/portmaster/spn/conf" +) + +func TestMain(m *testing.M) { + runningTests = true + conf.EnablePublicHub(true) // Make hub config available. + access.EnableTestMode() // Register test zone instead of real ones. + pmtesting.TestMain(m, module) +} diff --git a/spn/docks/op_capacity.go b/spn/docks/op_capacity.go new file mode 100644 index 00000000..a66ae617 --- /dev/null +++ b/spn/docks/op_capacity.go @@ -0,0 +1,356 @@ +package docks + +import ( + "bytes" + "context" + "sync/atomic" + "time" + + "github.com/tevino/abool" + + "github.com/safing/portbase/container" + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/terminal" +) + +const ( + // CapacityTestOpType is the type ID of the capacity test operation. + CapacityTestOpType = "capacity" + + defaultCapacityTestVolume = 50000000 // 50MB + maxCapacityTestVolume = 100000000 // 100MB + + defaultCapacityTestMaxTime = 5 * time.Second + maxCapacityTestMaxTime = 15 * time.Second + capacityTestTimeout = 30 * time.Second + + capacityTestMsgSize = 1000 + capacityTestSendTimeout = 1000 * time.Millisecond +) + +var ( + capacityTestSendData = make([]byte, capacityTestMsgSize) + capacityTestDataReceivedSignal = []byte("ACK") + + capacityTestRunning = abool.New() +) + +// CapacityTestOp is used for capacity test operations. +type CapacityTestOp struct { //nolint:maligned + terminal.OperationBase + + opts *CapacityTestOptions + + started bool + startTime time.Time + senderStarted bool + + recvQueue chan *terminal.Msg + dataReceived int + dataReceivedAckWasAckd bool + + dataSent *int64 + dataSentWasAckd *abool.AtomicBool + + testResult int + result chan *terminal.Error +} + +// CapacityTestOptions holds options for the capacity test. +type CapacityTestOptions struct { + TestVolume int + MaxTime time.Duration + testing bool +} + +// Type returns the type ID. +func (op *CapacityTestOp) Type() string { + return CapacityTestOpType +} + +func init() { + terminal.RegisterOpType(terminal.OperationFactory{ + Type: CapacityTestOpType, + Requires: terminal.IsCraneController, + Start: startCapacityTestOp, + }) +} + +// NewCapacityTestOp runs a capacity test. +func NewCapacityTestOp(t terminal.Terminal, opts *CapacityTestOptions) (*CapacityTestOp, *terminal.Error) { + // Check options. + if opts == nil { + opts = &CapacityTestOptions{ + TestVolume: defaultCapacityTestVolume, + MaxTime: defaultCapacityTestMaxTime, + } + } + + // Check if another test is already running. + if !opts.testing && !capacityTestRunning.SetToIf(false, true) { + return nil, terminal.ErrTryAgainLater.With("another capacity op is already running") + } + + // Create and init. + op := &CapacityTestOp{ + opts: opts, + recvQueue: make(chan *terminal.Msg), + dataSent: new(int64), + dataSentWasAckd: abool.New(), + result: make(chan *terminal.Error, 1), + } + + // Make capacity test request. + request, err := dsd.Dump(op.opts, dsd.CBOR) + if err != nil { + capacityTestRunning.UnSet() + return nil, terminal.ErrInternalError.With("failed to serialize capactity test options: %w", err) + } + + // Send test request. + tErr := t.StartOperation(op, container.New(request), 1*time.Second) + if tErr != nil { + capacityTestRunning.UnSet() + return nil, tErr + } + + // Start handler. + module.StartWorker("op capacity handler", op.handler) + + return op, nil +} + +func startCapacityTestOp(t terminal.Terminal, opID uint32, data *container.Container) (terminal.Operation, *terminal.Error) { + // Check if another test is already running. + if !capacityTestRunning.SetToIf(false, true) { + return nil, terminal.ErrTryAgainLater.With("another capacity op is already running") + } + + // Parse options. + opts := &CapacityTestOptions{} + _, err := dsd.Load(data.CompileData(), opts) + if err != nil { + capacityTestRunning.UnSet() + return nil, terminal.ErrMalformedData.With("failed to parse options: %w", err) + } + + // Check options. + if opts.TestVolume > maxCapacityTestVolume { + capacityTestRunning.UnSet() + return nil, terminal.ErrInvalidOptions.With("maximum volume exceeded") + } + if opts.MaxTime > maxCapacityTestMaxTime { + capacityTestRunning.UnSet() + return nil, terminal.ErrInvalidOptions.With("maximum maxtime exceeded") + } + + // Create operation. + op := &CapacityTestOp{ + opts: opts, + recvQueue: make(chan *terminal.Msg, 1000), + dataSent: new(int64), + dataSentWasAckd: abool.New(), + result: make(chan *terminal.Error, 1), + } + op.InitOperationBase(t, opID) + + // Start handler and sender. + op.senderStarted = true + module.StartWorker("op capacity handler", op.handler) + module.StartWorker("op capacity sender", op.sender) + + return op, nil +} + +func (op *CapacityTestOp) handler(ctx context.Context) error { + defer capacityTestRunning.UnSet() + + returnErr := terminal.ErrStopping + defer func() { + // Linters don't get that returnErr is used when directly used as defer. + op.Stop(op, returnErr) + }() + + var maxTestTimeReached <-chan time.Time + opTimeout := time.After(capacityTestTimeout) + + // Setup unit handling + var msg *terminal.Msg + defer msg.Finish() + + // Handle receives. + for { + msg.Finish() + + select { + case <-ctx.Done(): + returnErr = terminal.ErrCanceled + return nil + + case <-opTimeout: + returnErr = terminal.ErrTimeout + return nil + + case <-maxTestTimeReached: + returnErr = op.reportMeasuredCapacity() + return nil + + case msg = <-op.recvQueue: + // Record start time and start sender. + if !op.started { + op.started = true + op.startTime = time.Now() + maxTestTimeReached = time.After(op.opts.MaxTime) + if !op.senderStarted { + op.senderStarted = true + module.StartWorker("op capacity sender", op.sender) + } + } + + // Add to received data counter. + op.dataReceived += msg.Data.Length() + + // Check if we received the data received signal. + if msg.Data.Length() == len(capacityTestDataReceivedSignal) && + bytes.Equal(msg.Data.CompileData(), capacityTestDataReceivedSignal) { + op.dataSentWasAckd.Set() + } + + // Send the data received signal when we received the full test volume. + if op.dataReceived >= op.opts.TestVolume && !op.dataReceivedAckWasAckd { + tErr := op.Send(op.NewMsg(capacityTestDataReceivedSignal), capacityTestSendTimeout) + if tErr != nil { + returnErr = tErr.Wrap("failed to send data received signal") + return nil + } + atomic.AddInt64(op.dataSent, int64(len(capacityTestDataReceivedSignal))) + op.dataReceivedAckWasAckd = true + + // Flush last message. + op.Flush(10 * time.Second) + } + + // Check if we can complete the test. + if op.dataReceivedAckWasAckd && + op.dataSentWasAckd.IsSet() { + returnErr = op.reportMeasuredCapacity() + return nil + } + } + } +} + +func (op *CapacityTestOp) sender(ctx context.Context) error { + for { + // Send next chunk. + msg := op.NewMsg(capacityTestSendData) + msg.Unit.MakeHighPriority() + tErr := op.Send(msg, capacityTestSendTimeout) + if tErr != nil { + op.Stop(op, tErr.Wrap("failed to send capacity test data")) + return nil + } + + // Add to sent data counter and stop sending if sending is complete. + if atomic.AddInt64(op.dataSent, int64(len(capacityTestSendData))) >= int64(op.opts.TestVolume) { + return nil + } + + // Check if we have received an ack. + if op.dataSentWasAckd.IsSet() { + return nil + } + + // Check if op has ended. + if op.Stopped() { + return nil + } + } +} + +func (op *CapacityTestOp) reportMeasuredCapacity() *terminal.Error { + // Calculate lane capacity and set it. + timeNeeded := time.Since(op.startTime) + if timeNeeded <= 0 { + timeNeeded = 1 + } + duplexBits := float64((int64(op.dataReceived) + atomic.LoadInt64(op.dataSent)) * 8) + duplexNSBitRate := duplexBits / float64(timeNeeded) + bitRate := (duplexNSBitRate / 2) * float64(time.Second) + op.testResult = int(bitRate) + + // Save the result to the crane. + if controller, ok := op.Terminal().(*CraneControllerTerminal); ok { + if controller.Crane.ConnectedHub != nil { + controller.Crane.ConnectedHub.GetMeasurements().SetCapacity(op.testResult) + log.Infof( + "docks: measured capacity to %s: %.2f Mbit/s (%.2fMB down / %.2fMB up in %s)", + controller.Crane.ConnectedHub, + float64(op.testResult)/1000000, + float64(op.dataReceived)/1000000, + float64(atomic.LoadInt64(op.dataSent))/1000000, + timeNeeded, + ) + return nil + } else if controller.Crane.IsMine() { + return terminal.ErrInternalError.With("capacity operation was run on %s without a connected hub set", controller.Crane) + } + } else if !runningTests { + return terminal.ErrInternalError.With("capacity operation was run on terminal that is not a crane controller, but %T", op.Terminal()) + } + + return nil +} + +// Deliver delivers a message. +func (op *CapacityTestOp) Deliver(msg *terminal.Msg) *terminal.Error { + // Optimized delivery with 1s timeout. + select { + case op.recvQueue <- msg: + default: + select { + case op.recvQueue <- msg: + case <-time.After(1 * time.Second): + msg.Finish() + return terminal.ErrTimeout + } + } + return nil +} + +// HandleStop gives the operation the ability to cleanly shut down. +// The returned error is the error to send to the other side. +// Should never be called directly. Call Stop() instead. +func (op *CapacityTestOp) HandleStop(tErr *terminal.Error) (errorToSend *terminal.Error) { + // Return result to waiting routine. + select { + case op.result <- tErr: + default: + } + + // Drain the recvQueue to finish the message units. +drain: + for { + select { + case msg := <-op.recvQueue: + msg.Finish() + default: + select { + case msg := <-op.recvQueue: + msg.Finish() + case <-time.After(3 * time.Millisecond): + // Give some additional time buffer to drain the queue. + break drain + } + } + } + + // Return error as is. + return tErr +} + +// Result returns the result (end error) of the operation. +func (op *CapacityTestOp) Result() <-chan *terminal.Error { + return op.result +} diff --git a/spn/docks/op_capacity_test.go b/spn/docks/op_capacity_test.go new file mode 100644 index 00000000..1aaa1437 --- /dev/null +++ b/spn/docks/op_capacity_test.go @@ -0,0 +1,85 @@ +package docks + +import ( + "testing" + "time" + + "github.com/safing/portmaster/spn/terminal" +) + +var ( + testCapacityTestVolume = 1_000_000 + testCapacitytestMaxTime = 1 * time.Second +) + +func TestCapacityOp(t *testing.T) { //nolint:paralleltest // Performance test. + // Defaults. + testCapacityOp(t, &CapacityTestOptions{ + TestVolume: testCapacityTestVolume, + MaxTime: testCapacitytestMaxTime, + testing: true, + }) + + // Hit max time first. + testCapacityOp(t, &CapacityTestOptions{ + TestVolume: testCapacityTestVolume, + MaxTime: 100 * time.Millisecond, + testing: true, + }) + + // Hit volume first. + testCapacityOp(t, &CapacityTestOptions{ + TestVolume: 100_000, + MaxTime: testCapacitytestMaxTime, + testing: true, + }) +} + +func testCapacityOp(t *testing.T, opts *CapacityTestOptions) { + t.Helper() + + var ( + capTestDelay = 5 * time.Millisecond + capTestQueueSize uint32 = 10 + ) + + // Create test terminal pair. + a, b, err := terminal.NewSimpleTestTerminalPair( + capTestDelay, + int(capTestQueueSize), + &terminal.TerminalOpts{ + FlowControl: terminal.FlowControlDFQ, + FlowControlSize: capTestQueueSize, + }, + ) + if err != nil { + t.Fatalf("failed to create test terminal pair: %s", err) + } + + // Grant permission for op on remote terminal and start op. + b.GrantPermission(terminal.IsCraneController) + op, tErr := NewCapacityTestOp(a, opts) + if tErr != nil { + t.Fatalf("failed to start op: %s", err) + } + + // Wait for result and check error. + tErr = <-op.Result() + if !tErr.IsOK() { + t.Fatalf("op failed: %s", tErr) + } + t.Logf("measured capacity: %d bit/s", op.testResult) + + // Calculate expected bandwidth. + expectedBitsPerSecond := float64(capacityTestMsgSize*8*int64(capTestQueueSize)) / float64(capTestDelay) * float64(time.Second) + t.Logf("expected capacity: %f bit/s", expectedBitsPerSecond) + + // Check if measured bandwidth is within parameters. + if float64(op.testResult) > expectedBitsPerSecond*1.6 { + t.Fatal("measured capacity too high") + } + // TODO: Check if we can raise this to at least 90%. + if float64(op.testResult) < expectedBitsPerSecond*0.2 { + t.Fatal("measured capacity too low") + } +} diff --git a/spn/docks/op_expand.go b/spn/docks/op_expand.go new file mode 100644 index 00000000..4a96c766 --- /dev/null +++ b/spn/docks/op_expand.go @@ -0,0 +1,393 @@ +package docks + +import ( + "context" + "fmt" + "sync/atomic" + "time" + + "github.com/tevino/abool" + + "github.com/safing/portbase/container" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/terminal" +) + +// ExpandOpType is the type ID of the expand operation. +const ExpandOpType string = "expand" + +var activeExpandOps = new(int64) + +// ExpandOp is used to expand to another Hub. +type ExpandOp struct { + terminal.OperationBase + opts *terminal.TerminalOpts + + // ctx is the context of the Terminal. + ctx context.Context + // cancelCtx cancels ctx. + cancelCtx context.CancelFunc + + dataRelayed *uint64 + ended *abool.AtomicBool + + relayTerminal *ExpansionRelayTerminal + + // flowControl holds the flow control system. + flowControl terminal.FlowControl + // deliverProxy is populated with the configured deliver function + deliverProxy func(msg *terminal.Msg) *terminal.Error + // recvProxy is populated with the configured recv function + recvProxy func() <-chan *terminal.Msg + // sendProxy is populated with the configured send function + sendProxy func(msg *terminal.Msg, timeout time.Duration) +} + +// ExpansionRelayTerminal is a relay used for expansion. +type ExpansionRelayTerminal struct { + terminal.BareTerminal + + op *ExpandOp + + id uint32 + crane *Crane + + abandoning *abool.AtomicBool + + // flowControl holds the flow control system. + flowControl terminal.FlowControl + // deliverProxy is populated with the configured deliver function + deliverProxy func(msg *terminal.Msg) *terminal.Error + // recvProxy is populated with the configured recv function + recvProxy func() <-chan *terminal.Msg + // sendProxy is populated with the configured send function + sendProxy func(msg *terminal.Msg, timeout time.Duration) +} + +// Type returns the type ID. +func (op *ExpandOp) Type() string { + return ExpandOpType +} + +// ID returns the operation ID. +func (t *ExpansionRelayTerminal) ID() uint32 { + return t.id +} + +// Ctx returns the operation context. +func (op *ExpandOp) Ctx() context.Context { + return op.ctx +} + +// Ctx returns the relay terminal context. +func (t *ExpansionRelayTerminal) Ctx() context.Context { + return t.op.ctx +} + +// Deliver delivers a message to the relay operation. +func (op *ExpandOp) Deliver(msg *terminal.Msg) *terminal.Error { + return op.deliverProxy(msg) +} + +// Deliver delivers a message to the relay terminal. +func (t *ExpansionRelayTerminal) Deliver(msg *terminal.Msg) *terminal.Error { + return t.deliverProxy(msg) +} + +// Flush writes all data in the queues. +func (op *ExpandOp) Flush(timeout time.Duration) { + if op.flowControl != nil { + op.flowControl.Flush(timeout) + } +} + +// Flush writes all data in the queues. +func (t *ExpansionRelayTerminal) Flush(timeout time.Duration) { + if t.flowControl != nil { + t.flowControl.Flush(timeout) + } +} + +func init() { + terminal.RegisterOpType(terminal.OperationFactory{ + Type: ExpandOpType, + Requires: terminal.MayExpand, + Start: expand, + }) +} + +func expand(t terminal.Terminal, opID uint32, data *container.Container) (terminal.Operation, *terminal.Error) { + // Submit metrics. + newExpandOp.Inc() + + // Check if we are running a public hub. + if !conf.PublicHub() { + return nil, terminal.ErrPermissionDenied.With("expanding is only allowed on public hubs") + } + + // Parse destination hub ID. + dstData, err := data.GetNextBlock() + if err != nil { + return nil, terminal.ErrMalformedData.With("failed to parse destination: %w", err) + } + + // Parse terminal options. + opts, tErr := terminal.ParseTerminalOpts(data) + if tErr != nil { + return nil, tErr.Wrap("failed to parse terminal options") + } + + // Get crane with destination. + relayCrane := GetAssignedCrane(string(dstData)) + if relayCrane == nil { + return nil, terminal.ErrHubUnavailable.With("no crane assigned to %q", string(dstData)) + } + + // TODO: Expand outside of hot path. + + // Create operation and terminal. + op := &ExpandOp{ + opts: opts, + dataRelayed: new(uint64), + ended: abool.New(), + relayTerminal: &ExpansionRelayTerminal{ + crane: relayCrane, + id: relayCrane.getNextTerminalID(), + abandoning: abool.New(), + }, + } + op.InitOperationBase(t, opID) + op.ctx, op.cancelCtx = context.WithCancel(t.Ctx()) + op.relayTerminal.op = op + + // Create flow control. + switch opts.FlowControl { + case terminal.FlowControlDFQ: + // Operation + op.flowControl = terminal.NewDuplexFlowQueue(op.ctx, opts.FlowControlSize, op.submitBackwardUpstream) + op.deliverProxy = op.flowControl.Deliver + op.recvProxy = op.flowControl.Receive + op.sendProxy = op.submitBackwardFlowControl + // Relay Terminal + op.relayTerminal.flowControl = terminal.NewDuplexFlowQueue(op.ctx, opts.FlowControlSize, op.submitForwardUpstream) + op.relayTerminal.deliverProxy = op.relayTerminal.flowControl.Deliver + op.relayTerminal.recvProxy = op.relayTerminal.flowControl.Receive + op.relayTerminal.sendProxy = op.submitForwardFlowControl + case terminal.FlowControlNone: + // Operation + deliverToOp := make(chan *terminal.Msg, opts.FlowControlSize) + op.deliverProxy = terminal.MakeDirectDeliveryDeliverFunc(op.ctx, deliverToOp) + op.recvProxy = terminal.MakeDirectDeliveryRecvFunc(deliverToOp) + op.sendProxy = op.submitBackwardUpstream + // Relay Terminal + deliverToRelay := make(chan *terminal.Msg, opts.FlowControlSize) + op.relayTerminal.deliverProxy = terminal.MakeDirectDeliveryDeliverFunc(op.ctx, deliverToRelay) + op.relayTerminal.recvProxy = terminal.MakeDirectDeliveryRecvFunc(deliverToRelay) + op.relayTerminal.sendProxy = op.submitForwardUpstream + case terminal.FlowControlDefault: + fallthrough + default: + return nil, terminal.ErrInternalError.With("unknown flow control type %d", opts.FlowControl) + } + + // Establish terminal on destination. + newInitData, tErr := opts.Pack() + if tErr != nil { + return nil, terminal.ErrInternalError.With("failed to re-pack options: %w", err) + } + tErr = op.relayTerminal.crane.EstablishNewTerminal(op.relayTerminal, newInitData) + if tErr != nil { + return nil, tErr + } + + // Start workers. + module.StartWorker("expand op forward relay", op.forwardHandler) + module.StartWorker("expand op backward relay", op.backwardHandler) + if op.flowControl != nil { + op.flowControl.StartWorkers(module, "expand op") + } + if op.relayTerminal.flowControl != nil { + op.relayTerminal.flowControl.StartWorkers(module, "expand op terminal") + } + + return op, nil +} + +func (op *ExpandOp) submitForwardFlowControl(msg *terminal.Msg, timeout time.Duration) { + err := op.relayTerminal.flowControl.Send(msg, timeout) + if err != nil { + msg.Finish() + op.Stop(op, err.Wrap("failed to submit to forward flow control")) + } +} + +func (op *ExpandOp) submitBackwardFlowControl(msg *terminal.Msg, timeout time.Duration) { + err := op.flowControl.Send(msg, timeout) + if err != nil { + msg.Finish() + op.Stop(op, err.Wrap("failed to submit to backward flow control")) + } +} + +func (op *ExpandOp) submitForwardUpstream(msg *terminal.Msg, timeout time.Duration) { + msg.FlowID = op.relayTerminal.id + if msg.Unit.IsHighPriority() && op.opts.UsePriorityDataMsgs { + msg.Type = terminal.MsgTypePriorityData + } else { + msg.Type = terminal.MsgTypeData + } + err := op.relayTerminal.crane.Send(msg, timeout) + if err != nil { + msg.Finish() + op.Stop(op, err.Wrap("failed to submit to forward upstream")) + } +} + +func (op *ExpandOp) submitBackwardUpstream(msg *terminal.Msg, timeout time.Duration) { + msg.FlowID = op.relayTerminal.id + if msg.Unit.IsHighPriority() && op.opts.UsePriorityDataMsgs { + msg.Type = terminal.MsgTypePriorityData + } else { + msg.Type = terminal.MsgTypeData + msg.Unit.RemovePriority() + } + // Note: op.Send() will transform high priority units to priority data msgs. + err := op.Send(msg, timeout) + if err != nil { + msg.Finish() + op.Stop(op, err.Wrap("failed to submit to backward upstream")) + } +} + +func (op *ExpandOp) forwardHandler(_ context.Context) error { + // Metrics setup and submitting. + atomic.AddInt64(activeExpandOps, 1) + started := time.Now() + defer func() { + atomic.AddInt64(activeExpandOps, -1) + expandOpDurationHistogram.UpdateDuration(started) + expandOpRelayedDataHistogram.Update(float64(atomic.LoadUint64(op.dataRelayed))) + }() + + for { + select { + case msg := <-op.recvProxy(): + // Debugging: + // log.Debugf("spn/testing: forwarding at %s: %s", op.FmtID(), spew.Sdump(c.CompileData())) + + // Wait for processing slot. + msg.Unit.WaitForSlot() + + // Count relayed data for metrics. + atomic.AddUint64(op.dataRelayed, uint64(msg.Data.Length())) + + // Receive data from the origin and forward it to the relay. + op.relayTerminal.sendProxy(msg, 1*time.Minute) + + case <-op.ctx.Done(): + return nil + } + } +} + +func (op *ExpandOp) backwardHandler(_ context.Context) error { + for { + select { + case msg := <-op.relayTerminal.recvProxy(): + // Debugging: + // log.Debugf("spn/testing: backwarding at %s: %s", op.FmtID(), spew.Sdump(c.CompileData())) + + // Wait for processing slot. + msg.Unit.WaitForSlot() + + // Count relayed data for metrics. + atomic.AddUint64(op.dataRelayed, uint64(msg.Data.Length())) + + // Receive data from the relay and forward it to the origin. + op.sendProxy(msg, 1*time.Minute) + + case <-op.ctx.Done(): + return nil + } + } +} + +// HandleStop gives the operation the ability to cleanly shut down. +// The returned error is the error to send to the other side. +// Should never be called directly. Call Stop() instead. +func (op *ExpandOp) HandleStop(err *terminal.Error) (errorToSend *terminal.Error) { + // Flush all messages before stopping. + op.Flush(1 * time.Minute) + op.relayTerminal.Flush(1 * time.Minute) + + // Stop connected workers. + op.cancelCtx() + + // Abandon connected terminal. + op.relayTerminal.Abandon(nil) + + // Add context to error. + if err.IsError() { + return err.Wrap("relay operation failed with") + } + return err +} + +// Abandon shuts down the terminal unregistering it from upstream and calling HandleAbandon(). +func (t *ExpansionRelayTerminal) Abandon(err *terminal.Error) { + if t.abandoning.SetToIf(false, true) { + module.StartWorker("terminal abandon procedure", func(_ context.Context) error { + t.handleAbandonProcedure(err) + return nil + }) + } +} + +// HandleAbandon gives the terminal the ability to cleanly shut down. +// The returned error is the error to send to the other side. +// Should never be called directly. Call Abandon() instead. +func (t *ExpansionRelayTerminal) HandleAbandon(err *terminal.Error) (errorToSend *terminal.Error) { + // Stop the connected relay operation. + t.op.Stop(t.op, err) + + // Add context to error. + if err.IsError() { + return err.Wrap("relay terminal failed with") + } + return err +} + +// HandleDestruction gives the terminal the ability to clean up. +// The terminal has already fully shut down at this point. +// Should never be called directly. Call Abandon() instead. +func (t *ExpansionRelayTerminal) HandleDestruction(err *terminal.Error) {} + +func (t *ExpansionRelayTerminal) handleAbandonProcedure(err *terminal.Error) { + // Call operation stop handle function for proper shutdown cleaning up. + err = t.HandleAbandon(err) + + // Flush all messages before stopping. + t.Flush(1 * time.Minute) + + // Send error to the connected Operation, if the error is internal. + if !err.IsExternal() { + if err == nil { + err = terminal.ErrStopping + } + + msg := terminal.NewMsg(err.Pack()) + msg.FlowID = t.ID() + msg.Type = terminal.MsgTypeStop + t.op.submitForwardUpstream(msg, 1*time.Second) + } +} + +// FmtID returns the expansion ID hierarchy. +func (op *ExpandOp) FmtID() string { + return fmt.Sprintf("%s>%d %s#%d", op.Terminal().FmtID(), op.ID(), op.relayTerminal.crane.ID, op.relayTerminal.id) +} + +// FmtID returns the expansion ID hierarchy. +func (t *ExpansionRelayTerminal) FmtID() string { + return fmt.Sprintf("%s#%d", t.crane.ID, t.id) +} diff --git a/spn/docks/op_latency.go b/spn/docks/op_latency.go new file mode 100644 index 00000000..02c38f78 --- /dev/null +++ b/spn/docks/op_latency.go @@ -0,0 +1,298 @@ +package docks + +import ( + "bytes" + "context" + "fmt" + "time" + + "github.com/safing/portbase/container" + "github.com/safing/portbase/formats/varint" + "github.com/safing/portbase/log" + "github.com/safing/portbase/rng" + "github.com/safing/portmaster/spn/terminal" +) + +const ( + // LatencyTestOpType is the type ID of the latency test operation. + LatencyTestOpType = "latency" + + latencyPingRequest = 1 + latencyPingResponse = 2 + + latencyTestNonceSize = 16 + latencyTestRuns = 10 +) + +var ( + latencyTestPauseDuration = 1 * time.Second + latencyTestOpTimeout = latencyTestRuns * latencyTestPauseDuration * 3 +) + +// LatencyTestOp is used to measure latency. +type LatencyTestOp struct { + terminal.OperationBase +} + +// LatencyTestClientOp is the client version of LatencyTestOp. +type LatencyTestClientOp struct { + LatencyTestOp + + lastPingSentAt time.Time + lastPingNonce []byte + measuredLatencies []time.Duration + responses chan *terminal.Msg + testResult time.Duration + + result chan *terminal.Error +} + +// Type returns the type ID. +func (op *LatencyTestOp) Type() string { + return LatencyTestOpType +} + +func init() { + terminal.RegisterOpType(terminal.OperationFactory{ + Type: LatencyTestOpType, + Requires: terminal.IsCraneController, + Start: startLatencyTestOp, + }) +} + +// NewLatencyTestOp runs a latency test. +func NewLatencyTestOp(t terminal.Terminal) (*LatencyTestClientOp, *terminal.Error) { + // Create and init. + op := &LatencyTestClientOp{ + responses: make(chan *terminal.Msg), + measuredLatencies: make([]time.Duration, 0, latencyTestRuns), + result: make(chan *terminal.Error, 1), + } + + // Make ping request. + pingRequest, err := op.createPingRequest() + if err != nil { + return nil, terminal.ErrInternalError.With("%w", err) + } + + // Send ping. + tErr := t.StartOperation(op, pingRequest, 1*time.Second) + if tErr != nil { + return nil, tErr + } + + // Start handler. + module.StartWorker("op latency handler", op.handler) + + return op, nil +} + +func (op *LatencyTestClientOp) handler(ctx context.Context) error { + returnErr := terminal.ErrStopping + defer func() { + // Linters don't get that returnErr is used when directly used as defer. + op.Stop(op, returnErr) + }() + + var nextTest <-chan time.Time + opTimeout := time.After(latencyTestOpTimeout) + + for { + select { + case <-ctx.Done(): + return nil + + case <-opTimeout: + return nil + + case <-nextTest: + // Create ping request msg. + pingRequest, err := op.createPingRequest() + if err != nil { + returnErr = terminal.ErrInternalError.With("%w", err) + return nil + } + msg := op.NewEmptyMsg() + msg.Unit.MakeHighPriority() + msg.Data = pingRequest + + // Send it. + tErr := op.Send(msg, latencyTestOpTimeout) + if tErr != nil { + returnErr = tErr.Wrap("failed to send ping request") + return nil + } + op.Flush(1 * time.Second) + + nextTest = nil + + case msg := <-op.responses: + // Check if the op ended. + if msg == nil { + return nil + } + + // Handle response + tErr := op.handleResponse(msg) + if tErr != nil { + returnErr = tErr + return nil //nolint:nilerr + } + + // Check if we have enough latency tests. + if len(op.measuredLatencies) >= latencyTestRuns { + returnErr = op.reportMeasuredLatencies() + return nil + } + + // Schedule next latency test, if not yet scheduled. + if nextTest == nil { + nextTest = time.After(latencyTestPauseDuration) + } + } + } +} + +func (op *LatencyTestClientOp) createPingRequest() (*container.Container, error) { + // Generate nonce. + nonce, err := rng.Bytes(latencyTestNonceSize) + if err != nil { + return nil, fmt.Errorf("failed to create ping nonce") + } + + // Set client request state. + op.lastPingSentAt = time.Now() + op.lastPingNonce = nonce + + return container.New( + varint.Pack8(latencyPingRequest), + nonce, + ), nil +} + +func (op *LatencyTestClientOp) handleResponse(msg *terminal.Msg) *terminal.Error { + defer msg.Finish() + + rType, err := msg.Data.GetNextN8() + if err != nil { + return terminal.ErrMalformedData.With("failed to get response type: %w", err) + } + + switch rType { + case latencyPingResponse: + // Check if the ping nonce matches. + if !bytes.Equal(op.lastPingNonce, msg.Data.CompileData()) { + return terminal.ErrIntegrity.With("ping nonce mismatch") + } + op.lastPingNonce = nil + // Save latency. + op.measuredLatencies = append(op.measuredLatencies, time.Since(op.lastPingSentAt)) + + return nil + default: + return terminal.ErrIncorrectUsage.With("unknown response type") + } +} + +func (op *LatencyTestClientOp) reportMeasuredLatencies() *terminal.Error { + // Find lowest value. + lowestLatency := time.Hour + for _, latency := range op.measuredLatencies { + if latency < lowestLatency { + lowestLatency = latency + } + } + op.testResult = lowestLatency + + // Save the result to the crane. + if controller, ok := op.Terminal().(*CraneControllerTerminal); ok { + if controller.Crane.ConnectedHub != nil { + controller.Crane.ConnectedHub.GetMeasurements().SetLatency(op.testResult) + log.Infof("spn/docks: measured latency to %s: %s", controller.Crane.ConnectedHub, op.testResult) + return nil + } else if controller.Crane.IsMine() { + return terminal.ErrInternalError.With("latency operation was run on %s without a connected hub set", controller.Crane) + } + } else if !runningTests { + return terminal.ErrInternalError.With("latency operation was run on terminal that is not a crane controller, but %T", op.Terminal()) + } + return nil +} + +// Deliver delivers a message to the operation. +func (op *LatencyTestClientOp) Deliver(msg *terminal.Msg) *terminal.Error { + // Optimized delivery with 1s timeout. + select { + case op.responses <- msg: + default: + select { + case op.responses <- msg: + case <-time.After(1 * time.Second): + return terminal.ErrTimeout + } + } + return nil +} + +// HandleStop gives the operation the ability to cleanly shut down. +// The returned error is the error to send to the other side. +// Should never be called directly. Call Stop() instead. +func (op *LatencyTestClientOp) HandleStop(tErr *terminal.Error) (errorToSend *terminal.Error) { + close(op.responses) + select { + case op.result <- tErr: + default: + } + return tErr +} + +// Result returns the result (end error) of the operation. +func (op *LatencyTestClientOp) Result() <-chan *terminal.Error { + return op.result +} + +func startLatencyTestOp(t terminal.Terminal, opID uint32, data *container.Container) (terminal.Operation, *terminal.Error) { + // Create operation. + op := &LatencyTestOp{} + op.InitOperationBase(t, opID) + + // Handle first request. + msg := op.NewEmptyMsg() + msg.Data = data + tErr := op.Deliver(msg) + if tErr != nil { + return nil, tErr + } + + return op, nil +} + +// Deliver delivers a message to the operation. +func (op *LatencyTestOp) Deliver(msg *terminal.Msg) *terminal.Error { + // Get request type. + rType, err := msg.Data.GetNextN8() + if err != nil { + return terminal.ErrMalformedData.With("failed to get response type: %w", err) + } + + switch rType { + case latencyPingRequest: + // Keep the nonce and just replace the msg type. + msg.Data.PrependNumber(latencyPingResponse) + msg.Type = terminal.MsgTypeData + msg.Unit.ReUse() + msg.Unit.MakeHighPriority() + + // Send response. + tErr := op.Send(msg, latencyTestOpTimeout) + if tErr != nil { + return tErr.Wrap("failed to send ping response") + } + op.Flush(1 * time.Second) + + return nil + + default: + return terminal.ErrIncorrectUsage.With("unknown request type") + } +} diff --git a/spn/docks/op_latency_test.go b/spn/docks/op_latency_test.go new file mode 100644 index 00000000..7a0b4ec7 --- /dev/null +++ b/spn/docks/op_latency_test.go @@ -0,0 +1,59 @@ +package docks + +import ( + "testing" + "time" + + "github.com/safing/portmaster/spn/terminal" +) + +func TestLatencyOp(t *testing.T) { + t.Parallel() + + var ( + latTestDelay = 10 * time.Millisecond + latTestQueueSize uint32 = 10 + ) + + // Reduce waiting time. + latencyTestPauseDuration = 100 * time.Millisecond + + // Create test terminal pair. + a, b, err := terminal.NewSimpleTestTerminalPair( + latTestDelay, + int(latTestQueueSize), + &terminal.TerminalOpts{ + FlowControl: terminal.FlowControlNone, + FlowControlSize: latTestQueueSize, + }, + ) + if err != nil { + t.Fatalf("failed to create test terminal pair: %s", err) + } + + // Grant permission for op on remote terminal and start op. + b.GrantPermission(terminal.IsCraneController) + op, tErr := NewLatencyTestOp(a) + if tErr != nil { + t.Fatalf("failed to start op: %s", err) + } + + // Wait for result and check error. + tErr = <-op.Result() + if tErr.IsError() { + t.Fatalf("op failed: %s", tErr) + } + t.Logf("measured latency: %f ms", float64(op.testResult)/float64(time.Millisecond)) + + // Calculate expected latency. + expectedLatency := float64(latTestDelay * 2) + t.Logf("expected latency: %f ms", expectedLatency/float64(time.Millisecond)) + + // Check if measured latency is within parameters. + if float64(op.testResult) > expectedLatency*1.2 { + t.Fatal("measured latency too high") + } + if float64(op.testResult) < expectedLatency*0.9 { + t.Fatal("measured latency too low") + } +} diff --git a/spn/docks/op_sync_state.go b/spn/docks/op_sync_state.go new file mode 100644 index 00000000..43530803 --- /dev/null +++ b/spn/docks/op_sync_state.go @@ -0,0 +1,150 @@ +package docks + +import ( + "context" + "time" + + "github.com/safing/portbase/container" + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/terminal" +) + +// SyncStateOpType is the type ID of the sync state operation. +const SyncStateOpType = "sync/state" + +// SyncStateOp is used to sync the crane state. +type SyncStateOp struct { + terminal.OneOffOperationBase +} + +// SyncStateMessage holds the sync data. +type SyncStateMessage struct { + Stopping bool + RequestStopping bool +} + +// Type returns the type ID. +func (op *SyncStateOp) Type() string { + return SyncStateOpType +} + +func init() { + terminal.RegisterOpType(terminal.OperationFactory{ + Type: SyncStateOpType, + Requires: terminal.IsCraneController, + Start: runSyncStateOp, + }) +} + +// startSyncStateOp starts a worker that runs the sync state operation. +func (crane *Crane) startSyncStateOp() { + module.StartWorker("sync crane state", func(ctx context.Context) error { + tErr := crane.Controller.SyncState(ctx) + if tErr != nil { + return tErr + } + + return nil + }) +} + +// SyncState runs a sync state operation. +func (controller *CraneControllerTerminal) SyncState(ctx context.Context) *terminal.Error { + // Check if we are a public Hub, whether we own the crane and whether the lane is public too. + if !conf.PublicHub() || !controller.Crane.Public() { + return nil + } + + // Create and init. + op := &SyncStateOp{} + op.Init() + + // Get optimization states. + requestStopping := false + func() { + controller.Crane.NetState.lock.Lock() + defer controller.Crane.NetState.lock.Unlock() + + requestStopping = controller.Crane.NetState.stoppingRequested + }() + + // Create sync message. + msg := &SyncStateMessage{ + Stopping: controller.Crane.stopping.IsSet(), + RequestStopping: requestStopping, + } + data, err := dsd.Dump(msg, dsd.CBOR) + if err != nil { + return terminal.ErrInternalError.With("%w", err) + } + + // Send message. + tErr := controller.StartOperation(op, container.New(data), 30*time.Second) + if tErr != nil { + return tErr + } + + // Wait for reply + select { + case tErr = <-op.Result: + if tErr.IsError() { + return tErr + } + return nil + case <-ctx.Done(): + return nil + case <-time.After(1 * time.Minute): + return terminal.ErrTimeout.With("timed out while waiting for sync crane result") + } +} + +func runSyncStateOp(t terminal.Terminal, opID uint32, data *container.Container) (terminal.Operation, *terminal.Error) { + // Check if we are a on a crane controller. + var ok bool + var controller *CraneControllerTerminal + if controller, ok = t.(*CraneControllerTerminal); !ok { + return nil, terminal.ErrIncorrectUsage.With("can only be used with a crane controller") + } + + // Check if we are a public Hub and whether the lane is public too. + if !conf.PublicHub() || !controller.Crane.Public() { + return nil, terminal.ErrPermissionDenied.With("only public lanes can sync crane status") + } + + // Load message. + syncState := &SyncStateMessage{} + _, err := dsd.Load(data.CompileData(), syncState) + if err != nil { + return nil, terminal.ErrMalformedData.With("failed to load sync state message: %w", err) + } + + // Apply optimization state. + controller.Crane.NetState.lock.Lock() + defer controller.Crane.NetState.lock.Unlock() + controller.Crane.NetState.stoppingRequestedByPeer = syncState.RequestStopping + + // Apply crane state only when we don't own the crane. + if !controller.Crane.IsMine() { + // Apply sync state. + var changed bool + if syncState.Stopping { + if controller.Crane.stopping.SetToIf(false, true) { + controller.Crane.NetState.markedStoppingAt = time.Now() + changed = true + } + } else { + if controller.Crane.stopping.SetToIf(true, false) { + controller.Crane.NetState.markedStoppingAt = time.Time{} + changed = true + } + } + + // Notify of change. + if changed { + controller.Crane.NotifyUpdate() + } + } + + return nil, nil +} diff --git a/spn/docks/op_whoami.go b/spn/docks/op_whoami.go new file mode 100644 index 00000000..baf5204c --- /dev/null +++ b/spn/docks/op_whoami.go @@ -0,0 +1,135 @@ +package docks + +import ( + "time" + + "github.com/safing/portbase/container" + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portmaster/spn/terminal" +) + +const ( + // WhoAmIType is the type ID of the latency test operation. + WhoAmIType = "whoami" + + whoAmITimeout = 3 * time.Second +) + +// WhoAmIOp is used to request some metadata about the other side. +type WhoAmIOp struct { + terminal.OneOffOperationBase + + response *WhoAmIResponse +} + +// WhoAmIResponse is a whoami response. +type WhoAmIResponse struct { + // Timestamp in nanoseconds + Timestamp int64 `cbor:"t,omitempty" json:"t,omitempty"` + + // Addr is the remote address as reported by the crane terminal (IP and port). + Addr string `cbor:"a,omitempty" json:"a,omitempty"` +} + +// Type returns the type ID. +func (op *WhoAmIOp) Type() string { + return WhoAmIType +} + +func init() { + terminal.RegisterOpType(terminal.OperationFactory{ + Type: WhoAmIType, + Start: startWhoAmI, + }) +} + +// WhoAmI executes a whoami operation and returns the response. +func WhoAmI(t terminal.Terminal) (*WhoAmIResponse, *terminal.Error) { + whoami, err := NewWhoAmIOp(t) + if err.IsError() { + return nil, err + } + + // Wait for response. + select { + case tErr := <-whoami.Result: + if tErr.IsError() { + return nil, tErr + } + return whoami.response, nil + case <-time.After(whoAmITimeout * 2): + return nil, terminal.ErrTimeout + } +} + +// NewWhoAmIOp starts a new whoami operation. +func NewWhoAmIOp(t terminal.Terminal) (*WhoAmIOp, *terminal.Error) { + // Create operation and init. + op := &WhoAmIOp{} + op.OneOffOperationBase.Init() + + // Send ping. + tErr := t.StartOperation(op, nil, whoAmITimeout) + if tErr != nil { + return nil, tErr + } + + return op, nil +} + +// Deliver delivers a message to the operation. +func (op *WhoAmIOp) Deliver(msg *terminal.Msg) *terminal.Error { + defer msg.Finish() + + // Parse response. + response := &WhoAmIResponse{} + _, err := dsd.Load(msg.Data.CompileData(), response) + if err != nil { + return terminal.ErrMalformedData.With("failed to parse ping response: %w", err) + } + + op.response = response + return terminal.ErrExplicitAck +} + +func startWhoAmI(t terminal.Terminal, opID uint32, data *container.Container) (terminal.Operation, *terminal.Error) { + // Get crane terminal, if available. + ct, _ := t.(*CraneTerminal) + + // Create response. + r := &WhoAmIResponse{ + Timestamp: time.Now().UnixNano(), + } + if ct != nil { + r.Addr = ct.RemoteAddr().String() + } + response, err := dsd.Dump(r, dsd.CBOR) + if err != nil { + return nil, terminal.ErrInternalError.With("failed to create whoami response: %w", err) + } + + // Send response. + msg := terminal.NewMsg(response) + msg.FlowID = opID + msg.Unit.MakeHighPriority() + if terminal.UsePriorityDataMsgs { + msg.Type = terminal.MsgTypePriorityData + } + tErr := t.Send(msg, whoAmITimeout) + if tErr != nil { + // Finish message unit on failure. + msg.Finish() + return nil, tErr.With("failed to send ping response") + } + + // Operation is just one response and finished successfully. + return nil, nil +} + +// HandleStop gives the operation the ability to cleanly shut down. +// The returned error is the error to send to the other side. +// Should never be called directly. Call Stop() instead. +func (op *WhoAmIOp) HandleStop(err *terminal.Error) (errorToSend *terminal.Error) { + // Continue with usual handling of inherited base. + return op.OneOffOperationBase.HandleStop(err) +} diff --git a/spn/docks/op_whoami_test.go b/spn/docks/op_whoami_test.go new file mode 100644 index 00000000..9ce32763 --- /dev/null +++ b/spn/docks/op_whoami_test.go @@ -0,0 +1,24 @@ +package docks + +import ( + "testing" + + "github.com/safing/portmaster/spn/terminal" +) + +func TestWhoAmIOp(t *testing.T) { + t.Parallel() + + // Create test terminal pair. + a, _, err := terminal.NewSimpleTestTerminalPair(0, 0, nil) + if err != nil { + t.Fatalf("failed to create test terminal pair: %s", err) + } + + // Run op. + resp, tErr := WhoAmI(a) + if tErr.IsError() { + t.Fatal(tErr) + } + t.Logf("whoami: %+v", resp) +} diff --git a/spn/docks/terminal_expansion.go b/spn/docks/terminal_expansion.go new file mode 100644 index 00000000..16895a83 --- /dev/null +++ b/spn/docks/terminal_expansion.go @@ -0,0 +1,150 @@ +package docks + +import ( + "fmt" + "sync" + "time" + + "github.com/tevino/abool" + + "github.com/safing/portbase/container" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/terminal" +) + +// ExpansionTerminal is used for expanding to another Hub. +type ExpansionTerminal struct { + *terminal.TerminalBase + + relayOp *ExpansionTerminalRelayOp + + changeNotifyFuncReady *abool.AtomicBool + changeNotifyFunc func() + + reachableChecked time.Time + reachableLock sync.Mutex +} + +// ExpansionTerminalRelayOp is the operation that connects to the relay. +type ExpansionTerminalRelayOp struct { + terminal.OperationBase + + expansionTerminal *ExpansionTerminal +} + +// Type returns the type ID. +func (op *ExpansionTerminalRelayOp) Type() string { + return ExpandOpType +} + +// ExpandTo initiates an expansion. +func ExpandTo(from terminal.Terminal, routeTo string, encryptFor *hub.Hub) (*ExpansionTerminal, *terminal.Error) { + // First, create the local endpoint terminal to generate the init data. + + // Create options and bare expansion terminal. + opts := terminal.DefaultExpansionTerminalOpts() + opts.Encrypt = encryptFor != nil + expansion := &ExpansionTerminal{ + changeNotifyFuncReady: abool.New(), + } + expansion.relayOp = &ExpansionTerminalRelayOp{ + expansionTerminal: expansion, + } + + // Create base terminal for expansion. + base, initData, tErr := terminal.NewLocalBaseTerminal( + module.Ctx, + 0, // Ignore; The ID of the operation is used for communication. + from.FmtID(), + encryptFor, + opts, + expansion.relayOp, + ) + if tErr != nil { + return nil, tErr.Wrap("failed to create expansion terminal base") + } + expansion.TerminalBase = base + base.SetTerminalExtension(expansion) + base.SetTimeout(defaultTerminalIdleTimeout) + + // Second, start the actual relay operation. + + // Create setup message for relay operation. + opInitData := container.New() + opInitData.AppendAsBlock([]byte(routeTo)) + opInitData.AppendContainer(initData) + + // Start relay operation on connected Hub. + tErr = from.StartOperation(expansion.relayOp, opInitData, 5*time.Second) + if tErr != nil { + return nil, tErr.Wrap("failed to start expansion operation") + } + + // Start Workers. + base.StartWorkers(module, "expansion terminal") + + return expansion, nil +} + +// SetChangeNotifyFunc sets a callback function that is called when the terminal state changes. +func (t *ExpansionTerminal) SetChangeNotifyFunc(f func()) { + if t.changeNotifyFuncReady.IsSet() { + return + } + t.changeNotifyFunc = f + t.changeNotifyFuncReady.Set() +} + +// NeedsReachableCheck returns whether the terminal should be checked if it is +// reachable via the existing network internal relayed connection. +func (t *ExpansionTerminal) NeedsReachableCheck(maxCheckAge time.Duration) bool { + t.reachableLock.Lock() + defer t.reachableLock.Unlock() + + return time.Since(t.reachableChecked) > maxCheckAge +} + +// MarkReachable marks the terminal as reachable via the existing network +// internal relayed connection. +func (t *ExpansionTerminal) MarkReachable() { + t.reachableLock.Lock() + defer t.reachableLock.Unlock() + + t.reachableChecked = time.Now() +} + +// HandleDestruction gives the terminal the ability to clean up. +// The terminal has already fully shut down at this point. +// Should never be called directly. Call Abandon() instead. +func (t *ExpansionTerminal) HandleDestruction(err *terminal.Error) { + // Trigger update of connected Pin. + if t.changeNotifyFuncReady.IsSet() { + t.changeNotifyFunc() + } + + // Stop the relay operation. + // The error message is arlready sent by the terminal. + t.relayOp.Stop(t.relayOp, nil) +} + +// CustomIDFormat formats the terminal ID. +func (t *ExpansionTerminal) CustomIDFormat() string { + return fmt.Sprintf("%s~%d", t.relayOp.Terminal().FmtID(), t.relayOp.ID()) +} + +// Deliver delivers a message to the operation. +func (op *ExpansionTerminalRelayOp) Deliver(msg *terminal.Msg) *terminal.Error { + // Proxy directly to expansion terminal. + return op.expansionTerminal.Deliver(msg) +} + +// HandleStop gives the operation the ability to cleanly shut down. +// The returned error is the error to send to the other side. +// Should never be called directly. Call Stop() instead. +func (op *ExpansionTerminalRelayOp) HandleStop(err *terminal.Error) (errorToSend *terminal.Error) { + // Stop the expansion terminal. + // The error message will be sent by the operation. + op.expansionTerminal.Abandon(nil) + + return err +} diff --git a/spn/docks/terminal_expansion_test.go b/spn/docks/terminal_expansion_test.go new file mode 100644 index 00000000..415716ea --- /dev/null +++ b/spn/docks/terminal_expansion_test.go @@ -0,0 +1,305 @@ +package docks + +import ( + "fmt" + "os" + "runtime/pprof" + "sync" + "testing" + "time" + + "github.com/safing/portmaster/spn/access" + "github.com/safing/portmaster/spn/cabin" + "github.com/safing/portmaster/spn/hub" + "github.com/safing/portmaster/spn/ships" + "github.com/safing/portmaster/spn/terminal" +) + +const defaultTestQueueSize = 200 + +func TestExpansion(t *testing.T) { + t.Parallel() + + // Test without and with encryption. + for _, encrypt := range []bool{false, true} { + // Test down/up separately and in parallel. + for _, parallel := range []bool{false, true} { + // Test with different flow controls. + for _, fc := range []struct { + flowControl terminal.FlowControlType + flowControlSize uint32 + }{ + { + flowControl: terminal.FlowControlNone, + flowControlSize: 5, + }, + { + flowControl: terminal.FlowControlDFQ, + flowControlSize: defaultTestQueueSize, + }, + } { + // Run tests with combined options. + testExpansion( + t, + "expansion-hop-test", + &terminal.TerminalOpts{ + Encrypt: encrypt, + Padding: 8, + FlowControl: fc.flowControl, + FlowControlSize: fc.flowControlSize, + }, + defaultTestQueueSize, + defaultTestQueueSize, + parallel, + ) + } + } + } + + stressTestOpts := &terminal.TerminalOpts{ + Encrypt: true, + Padding: 8, + FlowControl: terminal.FlowControlDFQ, + FlowControlSize: defaultTestQueueSize, + } + testExpansion(t, "expansion-stress-test-down", stressTestOpts, defaultTestQueueSize*100, 0, false) + testExpansion(t, "expansion-stress-test-up", stressTestOpts, 0, defaultTestQueueSize*100, false) + testExpansion(t, "expansion-stress-test-duplex", stressTestOpts, defaultTestQueueSize*100, defaultTestQueueSize*100, false) +} + +func testExpansion( //nolint:maintidx,thelper + t *testing.T, + testID string, + terminalOpts *terminal.TerminalOpts, + clientCountTo, + serverCountTo uint64, + inParallel bool, +) { + testID += fmt.Sprintf(":encrypt=%v,flowType=%d,parallel=%v", terminalOpts.Encrypt, terminalOpts.FlowControl, inParallel) + + var identity2, identity3, identity4 *cabin.Identity + var connectedHub2, connectedHub3, connectedHub4 *hub.Hub + if terminalOpts.Encrypt { + identity2, connectedHub2 = getTestIdentity(t) + identity3, connectedHub3 = getTestIdentity(t) + identity4, connectedHub4 = getTestIdentity(t) + } + + // Build ships and cranes. + optimalMinLoadSize = 100 + ship1to2 := ships.NewTestShip(!terminalOpts.Encrypt, 100) + ship2to3 := ships.NewTestShip(!terminalOpts.Encrypt, 100) + ship3to4 := ships.NewTestShip(!terminalOpts.Encrypt, 100) + + var crane1, crane2to1, crane2to3, crane3to2, crane3to4, crane4 *Crane + var craneWg sync.WaitGroup + craneWg.Add(6) + + go func() { + var err error + crane1, err = NewCrane(ship1to2, connectedHub2, nil) + if err != nil { + panic(fmt.Sprintf("expansion test %s could not create crane1: %s", testID, err)) + } + crane1.ID = "c1" + err = crane1.Start(module.Ctx) + if err != nil { + panic(fmt.Sprintf("expansion test %s could not start crane1: %s", testID, err)) + } + crane1.ship.MarkPublic() + craneWg.Done() + }() + go func() { + var err error + crane2to1, err = NewCrane(ship1to2.Reverse(), nil, identity2) + if err != nil { + panic(fmt.Sprintf("expansion test %s could not create crane2to1: %s", testID, err)) + } + crane2to1.ID = "c2to1" + err = crane2to1.Start(module.Ctx) + if err != nil { + panic(fmt.Sprintf("expansion test %s could not start crane2to1: %s", testID, err)) + } + crane2to1.ship.MarkPublic() + craneWg.Done() + }() + go func() { + var err error + crane2to3, err = NewCrane(ship2to3, connectedHub3, nil) + if err != nil { + panic(fmt.Sprintf("expansion test %s could not create crane2to3: %s", testID, err)) + } + crane2to3.ID = "c2to3" + err = crane2to3.Start(module.Ctx) + if err != nil { + panic(fmt.Sprintf("expansion test %s could not start crane2to3: %s", testID, err)) + } + crane2to3.ship.MarkPublic() + craneWg.Done() + }() + go func() { + var err error + crane3to2, err = NewCrane(ship2to3.Reverse(), nil, identity3) + if err != nil { + panic(fmt.Sprintf("expansion test %s could not create crane3to2: %s", testID, err)) + } + crane3to2.ID = "c3to2" + err = crane3to2.Start(module.Ctx) + if err != nil { + panic(fmt.Sprintf("expansion test %s could not start crane3to2: %s", testID, err)) + } + crane3to2.ship.MarkPublic() + craneWg.Done() + }() + go func() { + var err error + crane3to4, err = NewCrane(ship3to4, connectedHub4, nil) + if err != nil { + panic(fmt.Sprintf("expansion test %s could not create crane3to4: %s", testID, err)) + } + crane3to4.ID = "c3to4" + err = crane3to4.Start(module.Ctx) + if err != nil { + panic(fmt.Sprintf("expansion test %s could not start crane3to4: %s", testID, err)) + } + crane3to4.ship.MarkPublic() + craneWg.Done() + }() + go func() { + var err error + crane4, err = NewCrane(ship3to4.Reverse(), nil, identity4) + if err != nil { + panic(fmt.Sprintf("expansion test %s could not create crane4: %s", testID, err)) + } + crane4.ID = "c4" + err = crane4.Start(module.Ctx) + if err != nil { + panic(fmt.Sprintf("expansion test %s could not start crane4: %s", testID, err)) + } + crane4.ship.MarkPublic() + craneWg.Done() + }() + craneWg.Wait() + + // Assign cranes. + crane3HubID := testID + "-crane3HubID" + AssignCrane(crane3HubID, crane2to3) + crane4HubID := testID + "-crane4HubID" + AssignCrane(crane4HubID, crane3to4) + + t.Logf("expansion test %s: initial setup complete", testID) + + // Wait async for test to complete, print stack after timeout. + finished := make(chan struct{}) + go func() { + select { + case <-finished: + case <-time.After(30 * time.Second): + fmt.Printf("expansion test %s is taking too long, print stack:\n", testID) + _ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) + os.Exit(1) + } + }() + + // Start initial crane. + homeTerminal, initData, tErr := NewLocalCraneTerminal(crane1, nil, &terminal.TerminalOpts{}) + if tErr != nil { + t.Fatalf("expansion test %s failed to create home terminal: %s", testID, tErr) + } + tErr = crane1.EstablishNewTerminal(homeTerminal, initData) + if tErr != nil { + t.Fatalf("expansion test %s failed to connect home terminal: %s", testID, tErr) + } + + t.Logf("expansion test %s: home terminal setup complete", testID) + time.Sleep(100 * time.Millisecond) + + // Start counters for testing. + op0, tErr := terminal.NewCounterOp(homeTerminal, terminal.CounterOpts{ + ClientCountTo: clientCountTo, + ServerCountTo: serverCountTo, + }) + if tErr != nil { + t.Fatalf("expansion test %s failed to run counter op: %s", testID, tErr) + } + t.Logf("expansion test %s: home terminal counter setup complete", testID) + if !inParallel { + op0.Wait() + } + + // Start expansion to crane 3. + opAuthTo2, tErr := access.AuthorizeToTerminal(homeTerminal) + if tErr != nil { + t.Fatalf("expansion test %s failed to auth with home terminal: %s", testID, tErr) + } + tErr = <-opAuthTo2.Result + if tErr.IsError() { + t.Fatalf("expansion test %s failed to auth with home terminal: %s", testID, tErr) + } + expansionTerminalTo3, err := ExpandTo(homeTerminal, crane3HubID, connectedHub3) + if err != nil { + t.Fatalf("expansion test %s failed to expand to %s: %s", testID, crane3HubID, tErr) + } + + // Start counters for testing. + op1, tErr := terminal.NewCounterOp(expansionTerminalTo3, terminal.CounterOpts{ + ClientCountTo: clientCountTo, + ServerCountTo: serverCountTo, + }) + if tErr != nil { + t.Fatalf("expansion test %s failed to run counter op: %s", testID, tErr) + } + + t.Logf("expansion test %s: expansion to crane3 and counter setup complete", testID) + if !inParallel { + op1.Wait() + } + + // Start expansion to crane 4. + opAuthTo3, tErr := access.AuthorizeToTerminal(expansionTerminalTo3) + if tErr != nil { + t.Fatalf("expansion test %s failed to auth with extenstion terminal: %s", testID, tErr) + } + tErr = <-opAuthTo3.Result + if tErr.IsError() { + t.Fatalf("expansion test %s failed to auth with extenstion terminal: %s", testID, tErr) + } + + expansionTerminalTo4, err := ExpandTo(expansionTerminalTo3, crane4HubID, connectedHub4) + if err != nil { + t.Fatalf("expansion test %s failed to expand to %s: %s", testID, crane4HubID, tErr) + } + + // Start counters for testing. + op2, tErr := terminal.NewCounterOp(expansionTerminalTo4, terminal.CounterOpts{ + ClientCountTo: clientCountTo, + ServerCountTo: serverCountTo, + }) + if tErr != nil { + t.Fatalf("expansion test %s failed to run counter op: %s", testID, tErr) + } + + t.Logf("expansion test %s: expansion to crane4 and counter setup complete", testID) + op2.Wait() + + // Wait for op1 if not already. + if inParallel { + op0.Wait() + op1.Wait() + } + + // Wait for completion. + close(finished) + + // Wait a little so that all errors can be propagated, so we can truly see + // if we succeeded. + time.Sleep(100 * time.Millisecond) + + // Check errors. + if op1.Error != nil { + t.Fatalf("crane test %s counter op1 failed: %s", testID, op1.Error) + } + if op2.Error != nil { + t.Fatalf("crane test %s counter op2 failed: %s", testID, op2.Error) + } +} diff --git a/spn/hub/database.go b/spn/hub/database.go new file mode 100644 index 00000000..d4ca3f85 --- /dev/null +++ b/spn/hub/database.go @@ -0,0 +1,202 @@ +package hub + +import ( + "errors" + "fmt" + "sync" + "time" + + "github.com/safing/portbase/database" + "github.com/safing/portbase/database/iterator" + "github.com/safing/portbase/database/query" + "github.com/safing/portbase/database/record" +) + +var ( + db = database.NewInterface(&database.Options{ + Local: true, + Internal: true, + }) + + getFromNavigator func(mapName, hubID string) *Hub +) + +// MakeHubDBKey makes a hub db key. +func MakeHubDBKey(mapName, hubID string) string { + return fmt.Sprintf("cache:spn/hubs/%s/%s", mapName, hubID) +} + +// MakeHubMsgDBKey makes a hub msg db key. +func MakeHubMsgDBKey(mapName string, msgType MsgType, hubID string) string { + return fmt.Sprintf("cache:spn/msgs/%s/%s/%s", mapName, msgType, hubID) +} + +// SetNavigatorAccess sets a shortcut function to access hubs from the navigator instead of having go through the database. +// This also reduces the number of object in RAM and better caches parsed attributes. +func SetNavigatorAccess(fn func(mapName, hubID string) *Hub) { + if getFromNavigator == nil { + getFromNavigator = fn + } +} + +// GetHub get a Hub from the database - or the navigator, if configured. +func GetHub(mapName string, hubID string) (*Hub, error) { + if getFromNavigator != nil { + hub := getFromNavigator(mapName, hubID) + if hub != nil { + return hub, nil + } + } + + return GetHubByKey(MakeHubDBKey(mapName, hubID)) +} + +// GetHubByKey returns a hub by its raw DB key. +func GetHubByKey(key string) (*Hub, error) { + r, err := db.Get(key) + if err != nil { + return nil, err + } + + hub, err := EnsureHub(r) + if err != nil { + return nil, err + } + + return hub, nil +} + +// EnsureHub makes sure a database record is a Hub. +func EnsureHub(r record.Record) (*Hub, error) { + // unwrap + if r.IsWrapped() { + // only allocate a new struct, if we need it + newHub := &Hub{} + err := record.Unwrap(r, newHub) + if err != nil { + return nil, err + } + newHub = prepHub(newHub) + + // Fully validate when getting from database. + if err := newHub.Info.validateFormatting(); err != nil { + return nil, fmt.Errorf("announcement failed format validation: %w", err) + } + if err := newHub.Status.validateFormatting(); err != nil { + return nil, fmt.Errorf("status failed format validation: %w", err) + } + if err := newHub.Info.prepare(false); err != nil { + return nil, fmt.Errorf("failed to prepare announcement: %w", err) + } + + return newHub, nil + } + + // or adjust type + newHub, ok := r.(*Hub) + if !ok { + return nil, fmt.Errorf("record not of type *Hub, but %T", r) + } + newHub = prepHub(newHub) + + // Prepare only when already parsed. + if err := newHub.Info.prepare(false); err != nil { + return nil, fmt.Errorf("failed to prepare announcement: %w", err) + } + + // ensure status + return newHub, nil +} + +func prepHub(h *Hub) *Hub { + if h.Status == nil { + h.Status = &Status{} + } + h.Measurements = getSharedMeasurements(h.ID, h.Measurements) + return h +} + +// Save saves to Hub to the correct scope in the database. +func (h *Hub) Save() error { + if !h.KeyIsSet() { + h.SetKey(MakeHubDBKey(h.Map, h.ID)) + } + + return db.Put(h) +} + +// RemoveHubAndMsgs deletes a Hub and it's saved messages from the database. +func RemoveHubAndMsgs(mapName string, hubID string) (err error) { + err = db.Delete(MakeHubDBKey(mapName, hubID)) + if err != nil && !errors.Is(err, database.ErrNotFound) { + return fmt.Errorf("failed to delete main hub entry: %w", err) + } + + err = db.Delete(MakeHubMsgDBKey(mapName, MsgTypeAnnouncement, hubID)) + if err != nil && !errors.Is(err, database.ErrNotFound) { + return fmt.Errorf("failed to delete hub announcement data: %w", err) + } + + err = db.Delete(MakeHubMsgDBKey(mapName, MsgTypeStatus, hubID)) + if err != nil && !errors.Is(err, database.ErrNotFound) { + return fmt.Errorf("failed to delete hub status data: %w", err) + } + + return nil +} + +// HubMsg stores raw Hub messages. +type HubMsg struct { //nolint:golint + record.Base + sync.Mutex + + ID string + Map string + Type MsgType + Data []byte + + Received int64 +} + +// SaveHubMsg saves a raw (and signed) message received by another Hub. +func SaveHubMsg(id string, mapName string, msgType MsgType, data []byte) error { + // create wrapper record + msg := &HubMsg{ + ID: id, + Map: mapName, + Type: msgType, + Data: data, + Received: time.Now().Unix(), + } + // set key + msg.SetKey(MakeHubMsgDBKey(msg.Map, msg.Type, msg.ID)) + // save + return db.PutNew(msg) +} + +// QueryRawGossipMsgs queries the database for raw gossip messages. +func QueryRawGossipMsgs(mapName string, msgType MsgType) (it *iterator.Iterator, err error) { + it, err = db.Query(query.New(MakeHubMsgDBKey(mapName, msgType, ""))) + return +} + +// EnsureHubMsg makes sure a database record is a HubMsg. +func EnsureHubMsg(r record.Record) (*HubMsg, error) { + // unwrap + if r.IsWrapped() { + // only allocate a new struct, if we need it + newHubMsg := &HubMsg{} + err := record.Unwrap(r, newHubMsg) + if err != nil { + return nil, err + } + return newHubMsg, nil + } + + // or adjust type + newHubMsg, ok := r.(*HubMsg) + if !ok { + return nil, fmt.Errorf("record not of type *Hub, but %T", r) + } + return newHubMsg, nil +} diff --git a/spn/hub/errors.go b/spn/hub/errors.go new file mode 100644 index 00000000..276549e4 --- /dev/null +++ b/spn/hub/errors.go @@ -0,0 +1,21 @@ +package hub + +import "errors" + +var ( + // ErrMissingInfo signifies that the hub is missing the HubAnnouncement. + ErrMissingInfo = errors.New("hub has no announcement") + + // ErrMissingTransports signifies that the hub announcement did not specify any transports. + ErrMissingTransports = errors.New("hub announcement has no transports") + + // ErrMissingIPs signifies that the hub announcement did not specify any IPs, + // or none of the IPs is supported by the client. + ErrMissingIPs = errors.New("hub announcement has no (supported) IPs") + + // ErrTemporaryValidationError is returned when a validation error might be temporary. + ErrTemporaryValidationError = errors.New("temporary validation error") + + // ErrOldData is returned when received data is outdated. + ErrOldData = errors.New("") +) diff --git a/spn/hub/format.go b/spn/hub/format.go new file mode 100644 index 00000000..f36b3d0d --- /dev/null +++ b/spn/hub/format.go @@ -0,0 +1,69 @@ +package hub + +import ( + "fmt" + "net" + "regexp" + + "github.com/safing/portmaster/service/network/netutils" +) + +// BaselineCharset defines the permitted characters. +var BaselineCharset = regexp.MustCompile( + // Start of charset selection. + `^[` + + // Printable ASCII (character code 32-127), excluding common control characters of different languages: "$%&';<>\` and DELETE. + ` !#()*+,\-\./0-9:=?@A-Z[\]^_a-z{|}~` + + // Only latin characters from extended ASCII (character code 128-255). + `ŠŒŽšœžŸ¡¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ` + + // End of charset selection. + `]*$`, +) + +func checkStringFormat(fieldName, value string, maxLength int) error { + switch { + case len(value) > maxLength: + return fmt.Errorf("field %s with length of %d exceeds max length of %d", fieldName, len(value), maxLength) + case !BaselineCharset.MatchString(value): + return fmt.Errorf("field %s contains characters not permitted by baseline validation", fieldName) + default: + return nil + } +} + +func checkStringSliceFormat(fieldName string, value []string, maxLength, maxStringLength int) error { //nolint:unparam + if len(value) > maxLength { + return fmt.Errorf("field %s with array/slice length of %d exceeds max length of %d", fieldName, len(value), maxLength) + } + for _, s := range value { + if err := checkStringFormat(fieldName, s, maxStringLength); err != nil { + return err + } + } + return nil +} + +func checkByteSliceFormat(fieldName string, value []byte, maxLength int) error { + switch { + case len(value) > maxLength: + return fmt.Errorf("field %s with length of %d exceeds max length of %d", fieldName, len(value), maxLength) + default: + return nil + } +} + +func checkIPFormat(fieldName string, value net.IP) error { + // Check if there is an IP address. + if value == nil { + return nil + } + + switch { + case len(value) != 4 && len(value) != 16: + return fmt.Errorf("field %s has an invalid length of %d for an IP address", fieldName, len(value)) + case netutils.GetIPScope(value) == netutils.Invalid: + return fmt.Errorf("field %s holds an invalid IP address: %s", fieldName, value) + default: + return nil + } +} diff --git a/spn/hub/format_test.go b/spn/hub/format_test.go new file mode 100644 index 00000000..62b79635 --- /dev/null +++ b/spn/hub/format_test.go @@ -0,0 +1,81 @@ +package hub + +import ( + "fmt" + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCheckStringFormat(t *testing.T) { + t.Parallel() + + testSet := map[string]bool{ + // Printable ASCII (character code 32-127) + " ": true, "!": true, `"`: false, "#": true, "$": false, "%": false, "&": false, "'": false, + "(": true, ")": true, "*": true, "+": true, ",": true, "-": true, ".": true, "/": true, + "0": true, "1": true, "2": true, "3": true, "4": true, "5": true, "6": true, "7": true, + "8": true, "9": true, ":": true, ";": false, "<": false, "=": true, ">": false, "?": true, + "@": true, "A": true, "B": true, "C": true, "D": true, "E": true, "F": true, "G": true, + "H": true, "I": true, "J": true, "K": true, "L": true, "M": true, "N": true, "O": true, + "P": true, "Q": true, "R": true, "S": true, "T": true, "U": true, "V": true, "W": true, + "X": true, "Y": true, "Z": true, "[": true, `\`: false, "]": true, "^": true, "_": true, + "`": false, "a": true, "b": true, "c": true, "d": true, "e": true, "f": true, "g": true, + "h": true, "i": true, "j": true, "k": true, "l": true, "m": true, "n": true, "o": true, + "p": true, "q": true, "r": true, "s": true, "t": true, "u": true, "v": true, "w": true, + "x": true, "y": true, "z": true, "{": true, "|": true, "}": true, "~": true, + // Not testing for DELETE character. + + // Extended ASCII (character code 128-255) + "€": false, "‚": false, "ƒ": false, "„": false, "…": false, "†": false, "‡": false, "ˆ": false, + "‰": false, "Š": true, "‹": false, "Œ": true, "Ž": true, "‘": false, "’": false, "“": false, + "”": false, "•": false, "–": false, "—": false, "˜": false, "™": false, "š": true, "›": false, + "œ": true, "ž": true, "Ÿ": true, "¡": true, "¢": false, "£": false, "¤": false, "¥": false, + "¦": false, "§": false, "¨": false, "©": false, "ª": false, "«": false, "¬": false, "®": false, + "¯": false, "°": false, "±": false, "²": false, "³": false, "´": false, "µ": false, "¶": false, + "·": false, "¸": false, "¹": false, "º": false, "»": false, "¼": false, "½": false, "¾": false, + "¿": true, "À": true, "Á": true, "Â": true, "Ã": true, "Ä": true, "Å": true, "Æ": true, + "Ç": true, "È": true, "É": true, "Ê": true, "Ë": true, "Ì": true, "Í": true, "Î": true, + "Ï": true, "Ð": true, "Ñ": true, "Ò": true, "Ó": true, "Ô": true, "Õ": true, "Ö": true, + "×": false, "Ø": true, "Ù": true, "Ú": true, "Û": true, "Ü": true, "Ý": true, "Þ": true, + "ß": true, "à": true, "á": true, "â": true, "ã": true, "ä": true, "å": true, "æ": true, + "ç": true, "è": true, "é": true, "ê": true, "ë": true, "ì": true, "í": true, "î": true, + "ï": true, "ð": true, "ñ": true, "ò": true, "ó": true, "ô": true, "õ": true, "ö": true, + "÷": false, "ø": true, "ù": true, "ú": true, "û": true, "ü": true, "ý": true, "þ": true, + "ÿ": true, + } + + for testCharacter, isPermitted := range testSet { + if isPermitted { + assert.NoError(t, checkStringFormat(fmt.Sprintf("test character %q", testCharacter), testCharacter, 3)) + } else { + assert.Error(t, checkStringFormat(fmt.Sprintf("test character %q", testCharacter), testCharacter, 3)) + } + } +} + +func TestCheckIPFormat(t *testing.T) { + t.Parallel() + + // IPv4 + assert.NoError(t, checkIPFormat("test IP 1.1.1.1", net.IPv4(1, 1, 1, 1))) + assert.NoError(t, checkIPFormat("test IP 192.168.1.1", net.IPv4(192, 168, 1, 1))) + assert.Error(t, checkIPFormat("test IP 255.0.0.1", net.IPv4(255, 0, 0, 1))) + + // IPv6 + assert.NoError(t, checkIPFormat("test IP ::1", net.ParseIP("::1"))) + assert.NoError(t, checkIPFormat("test IP 2606:4700:4700::1111", net.ParseIP("2606:4700:4700::1111"))) + + // Invalid + assert.Error(t, checkIPFormat("test IP with length 3", net.IP([]byte{0, 0, 0}))) + assert.Error(t, checkIPFormat("test IP with length 5", net.IP([]byte{0, 0, 0, 0, 0}))) + assert.Error(t, checkIPFormat( + "test IP with length 15", + net.IP([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}), + )) + assert.Error(t, checkIPFormat( + "test IP with length 17", + net.IP([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}), + )) +} diff --git a/spn/hub/hub.go b/spn/hub/hub.go new file mode 100644 index 00000000..efc34cd0 --- /dev/null +++ b/spn/hub/hub.go @@ -0,0 +1,435 @@ +package hub + +import ( + "fmt" + "net" + "sync" + "time" + + "golang.org/x/exp/slices" + + "github.com/safing/jess" + "github.com/safing/portbase/database/record" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/service/profile/endpoints" +) + +// Scope is the network scope a Hub can be in. +type Scope uint8 + +const ( + // ScopeInvalid defines an invalid scope. + ScopeInvalid Scope = 0 + + // ScopeLocal identifies local Hubs. + ScopeLocal Scope = 1 + + // ScopePublic identifies public Hubs. + ScopePublic Scope = 2 + + // ScopeTest identifies Hubs for testing. + ScopeTest Scope = 0xFF +) + +const ( + obsoleteValidAfter = 30 * 24 * time.Hour + obsoleteInvalidAfter = 7 * 24 * time.Hour +) + +// MsgType defines the message type. +type MsgType string + +// Message Types. +const ( + MsgTypeAnnouncement = "announcement" + MsgTypeStatus = "status" +) + +// Hub represents a network node in the SPN. +type Hub struct { //nolint:maligned + sync.Mutex + record.Base + + ID string + PublicKey *jess.Signet + Map string + + Info *Announcement + Status *Status + + Measurements *Measurements + measurementsInitialized bool + + FirstSeen time.Time + VerifiedIPs bool + InvalidInfo bool + InvalidStatus bool +} + +// Announcement is the main message type to publish Hub Information. This only changes if updated manually. +type Announcement struct { + // Primary Key + // hash of public key + // must be checked if it matches the public key + ID string `cbor:"i"` // via jess.LabeledHash + + // PublicKey *jess.Signet + // PublicKey // if not part of signature + // Signature *jess.Letter + Timestamp int64 `cbor:"t"` // Unix timestamp in seconds + + // Node Information + Name string `cbor:"n"` // name of the node + Group string `cbor:"g,omitempty" json:",omitempty"` // person or organisation, who is in control of the node (should be same for all nodes of this person or organisation) + ContactAddress string `cbor:"ca,omitempty" json:",omitempty"` // contact possibility (recommended, but optional) + ContactService string `cbor:"cs,omitempty" json:",omitempty"` // type of service of the contact address, if not email + + // currently unused, but collected for later use + Hosters []string `cbor:"ho,omitempty" json:",omitempty"` // hoster supply chain (reseller, hosting provider, datacenter operator, ...) + Datacenter string `cbor:"dc,omitempty" json:",omitempty"` // datacenter will be bullshit checked + // Format: CC-COMPANY-INTERNALCODE + // Eg: DE-Hetzner-FSN1-DC5 + + // Network Location and Access + // If node is behind NAT (or similar), IP addresses must be configured + IPv4 net.IP `cbor:"ip4,omitempty" json:",omitempty"` // must be global and accessible + IPv6 net.IP `cbor:"ip6,omitempty" json:",omitempty"` // must be global and accessible + Transports []string `cbor:"tp,omitempty" json:",omitempty"` + // { + // "spn:17", + // "smtp:25", // also support "smtp://:25 + // "smtp:587", + // "imap:143", + // "http:80", + // "http://example.com:80", // HTTP (based): use full path for request + // "https:443", + // "ws:80", + // "wss://example.com:443/spn", + // } // protocols with metadata + parsedTransports []*Transport + + // Policies - default permit + Entry []string `cbor:"pi,omitempty" json:",omitempty"` + entryPolicy endpoints.Endpoints + // {"+ ", "- *"} + Exit []string `cbor:"po,omitempty" json:",omitempty"` + exitPolicy endpoints.Endpoints + // {"- * TCP/25", "- US"} + + // Flags holds flags that signify special states. + Flags []string `cbor:"f,omitempty" json:",omitempty"` +} + +// Copy returns a deep copy of the Announcement. +func (a *Announcement) Copy() *Announcement { + return &Announcement{ + ID: a.ID, + Timestamp: a.Timestamp, + Name: a.Name, + ContactAddress: a.ContactAddress, + ContactService: a.ContactService, + Hosters: slices.Clone(a.Hosters), + Datacenter: a.Datacenter, + IPv4: a.IPv4, + IPv6: a.IPv6, + Transports: slices.Clone(a.Transports), + parsedTransports: slices.Clone(a.parsedTransports), + Entry: slices.Clone(a.Entry), + entryPolicy: slices.Clone(a.entryPolicy), + Exit: slices.Clone(a.Exit), + exitPolicy: slices.Clone(a.exitPolicy), + Flags: slices.Clone(a.Flags), + } +} + +// GetInfo returns the hub info. +func (h *Hub) GetInfo() *Announcement { + h.Lock() + defer h.Unlock() + + return h.Info +} + +// GetStatus returns the hub status. +func (h *Hub) GetStatus() *Status { + h.Lock() + defer h.Unlock() + + return h.Status +} + +// GetMeasurements returns the hub measurements. +// This method should always be used instead of direct access. +func (h *Hub) GetMeasurements() *Measurements { + h.Lock() + defer h.Unlock() + + return h.GetMeasurementsWithLockedHub() +} + +// GetMeasurementsWithLockedHub returns the hub measurements. +// The caller must hold the lock to Hub. +// This method should always be used instead of direct access. +func (h *Hub) GetMeasurementsWithLockedHub() *Measurements { + if !h.measurementsInitialized { + h.Measurements = getSharedMeasurements(h.ID, h.Measurements) + h.Measurements.check() + h.measurementsInitialized = true + } + + return h.Measurements +} + +// Verified return whether the Hub has been verified. +func (h *Hub) Verified() bool { + h.Lock() + defer h.Unlock() + + return h.VerifiedIPs +} + +// String returns a human-readable representation of the Hub. +func (h *Hub) String() string { + h.Lock() + defer h.Unlock() + + return "" +} + +// StringWithoutLocking returns a human-readable representation of the Hub without locking it. +func (h *Hub) StringWithoutLocking() string { + return "" +} + +// Name returns a human-readable version of a Hub's name. This name will likely consist of two parts: the given name and the ending of the ID to make it unique. +func (h *Hub) Name() string { + h.Lock() + defer h.Unlock() + + return h.getName() +} + +func (h *Hub) getName() string { + // Check for a short ID that is sometimes used for testing. + if len(h.ID) < 8 { + return h.ID + } + + shortenedID := h.ID[len(h.ID)-8:len(h.ID)-4] + + "-" + + h.ID[len(h.ID)-4:] + + // Be more careful, as the Hub name is user input. + switch { + case h.Info.Name == "": + return shortenedID + case len(h.Info.Name) > 16: + return h.Info.Name[:16] + " " + shortenedID + default: + return h.Info.Name + " " + shortenedID + } +} + +// Obsolete returns if the Hub is obsolete and may be deleted. +func (h *Hub) Obsolete() bool { + h.Lock() + defer h.Unlock() + + // Check if Hub is valid. + var valid bool + switch { + case h.InvalidInfo: + case h.InvalidStatus: + case h.HasFlag(FlagOffline): + // Treat offline as invalid. + default: + valid = true + } + + // Check when Hub was last seen. + lastSeen := h.FirstSeen + if h.Status.Timestamp != 0 { + lastSeen = time.Unix(h.Status.Timestamp, 0) + } + + // Check if Hub is obsolete. + if valid { + return time.Now().Add(-obsoleteValidAfter).After(lastSeen) + } + return time.Now().Add(-obsoleteInvalidAfter).After(lastSeen) +} + +// HasFlag returns whether the Announcement or Status has the given flag set. +func (h *Hub) HasFlag(flagName string) bool { + switch { + case h.Status != nil && slices.Contains[[]string, string](h.Status.Flags, flagName): + return true + case h.Info != nil && slices.Contains[[]string, string](h.Info.Flags, flagName): + return true + } + return false +} + +// Equal returns whether the given Announcements are equal. +func (a *Announcement) Equal(b *Announcement) bool { + switch { + case a == nil || b == nil: + return false + case a.ID != b.ID: + return false + case a.Timestamp != b.Timestamp: + return false + case a.Name != b.Name: + return false + case a.ContactAddress != b.ContactAddress: + return false + case a.ContactService != b.ContactService: + return false + case !equalStringSlice(a.Hosters, b.Hosters): + return false + case a.Datacenter != b.Datacenter: + return false + case !a.IPv4.Equal(b.IPv4): + return false + case !a.IPv6.Equal(b.IPv6): + return false + case !equalStringSlice(a.Transports, b.Transports): + return false + case !equalStringSlice(a.Entry, b.Entry): + return false + case !equalStringSlice(a.Exit, b.Exit): + return false + case !equalStringSlice(a.Flags, b.Flags): + return false + default: + return true + } +} + +// validateFormatting check if all values conform to the basic format. +func (a *Announcement) validateFormatting() error { + if err := checkStringFormat("ID", a.ID, 255); err != nil { + return err + } + if err := checkStringFormat("Name", a.Name, 32); err != nil { + return err + } + if err := checkStringFormat("Group", a.Group, 32); err != nil { + return err + } + if err := checkStringFormat("ContactAddress", a.ContactAddress, 255); err != nil { + return err + } + if err := checkStringFormat("ContactService", a.ContactService, 255); err != nil { + return err + } + if err := checkStringSliceFormat("Hosters", a.Hosters, 255, 255); err != nil { + return err + } + if err := checkStringFormat("Datacenter", a.Datacenter, 255); err != nil { + return err + } + if err := checkIPFormat("IPv4", a.IPv4); err != nil { + return err + } + if err := checkIPFormat("IPv6", a.IPv6); err != nil { + return err + } + if err := checkStringSliceFormat("Transports", a.Transports, 255, 255); err != nil { + return err + } + if err := checkStringSliceFormat("Entry", a.Entry, 255, 255); err != nil { + return err + } + if err := checkStringSliceFormat("Exit", a.Exit, 255, 255); err != nil { + return err + } + if err := checkStringSliceFormat("Flags", a.Flags, 16, 32); err != nil { + return err + } + return nil +} + +// Prepare prepares the announcement by parsing policies and transports. +// If fields are already parsed, they will only be parsed again, when force is set to true. +func (a *Announcement) prepare(force bool) error { + var err error + + // Parse policies. + if len(a.entryPolicy) == 0 || force { + if a.entryPolicy, err = endpoints.ParseEndpoints(a.Entry); err != nil { + return fmt.Errorf("failed to parse entry policy: %w", err) + } + } + if len(a.exitPolicy) == 0 || force { + if a.exitPolicy, err = endpoints.ParseEndpoints(a.Exit); err != nil { + return fmt.Errorf("failed to parse exit policy: %w", err) + } + } + + // Parse transports. + if len(a.parsedTransports) == 0 || force { + parsed, errs := ParseTransports(a.Transports) + // Log parsing warnings. + for _, err := range errs { + log.Warningf("hub: Hub %s (%s) has configured an %s", a.Name, a.ID, err) + } + // Check if there are any valid transports. + if len(parsed) == 0 { + return ErrMissingTransports + } + a.parsedTransports = parsed + } + + return nil +} + +// EntryPolicy returns the Hub's entry policy. +func (a *Announcement) EntryPolicy() endpoints.Endpoints { + return a.entryPolicy +} + +// ExitPolicy returns the Hub's exit policy. +func (a *Announcement) ExitPolicy() endpoints.Endpoints { + return a.exitPolicy +} + +// ParsedTransports returns the Hub's parsed transports. +func (a *Announcement) ParsedTransports() []*Transport { + return a.parsedTransports +} + +// HasFlag returns whether the Announcement has the given flag set. +func (a *Announcement) HasFlag(flagName string) bool { + return slices.Contains[[]string, string](a.Flags, flagName) +} + +// String returns the string representation of the scope. +func (s Scope) String() string { + switch s { + case ScopeInvalid: + return "invalid" + case ScopeLocal: + return "local" + case ScopePublic: + return "public" + case ScopeTest: + return "test" + default: + return "unknown" + } +} + +func equalStringSlice(a, b []string) bool { + if len(a) != len(b) { + return false + } + + for i := 0; i < len(a); i++ { + if a[i] != b[i] { + return false + } + } + + return true +} diff --git a/spn/hub/hub_test.go b/spn/hub/hub_test.go new file mode 100644 index 00000000..70cc5b16 --- /dev/null +++ b/spn/hub/hub_test.go @@ -0,0 +1,79 @@ +package hub + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/safing/portbase/modules" + _ "github.com/safing/portmaster/service/core/base" + "github.com/safing/portmaster/service/core/pmtesting" +) + +func TestMain(m *testing.M) { + // TODO: We need the database module, so maybe set up a module for this package. + module := modules.Register("hub", nil, nil, nil, "base") + pmtesting.TestMain(m, module) +} + +func TestEquality(t *testing.T) { + t.Parallel() + + // empty match + a := &Announcement{} + assert.True(t, a.Equal(a), "should match itself") //nolint:gocritic // This is a test. + + // full match + a = &Announcement{ + ID: "a", + Timestamp: 1, + Name: "a", + ContactAddress: "a", + ContactService: "a", + Hosters: []string{"a", "b"}, + Datacenter: "a", + IPv4: net.IPv4(1, 2, 3, 4), + IPv6: net.ParseIP("::1"), + Transports: []string{"a", "b"}, + Entry: []string{"a", "b"}, + Exit: []string{"a", "b"}, + } + assert.True(t, a.Equal(a), "should match itself") //nolint:gocritic // This is a test. + + // no match + b := &Announcement{ID: "b"} + assert.False(t, a.Equal(b), "should not match") + b = &Announcement{Timestamp: 2} + assert.False(t, a.Equal(b), "should not match") + b = &Announcement{Name: "b"} + assert.False(t, a.Equal(b), "should not match") + b = &Announcement{ContactAddress: "b"} + assert.False(t, a.Equal(b), "should not match") + b = &Announcement{ContactService: "b"} + assert.False(t, a.Equal(b), "should not match") + b = &Announcement{Hosters: []string{"b", "c"}} + assert.False(t, a.Equal(b), "should not match") + b = &Announcement{Datacenter: "b"} + assert.False(t, a.Equal(b), "should not match") + b = &Announcement{IPv4: net.IPv4(1, 2, 3, 5)} + assert.False(t, a.Equal(b), "should not match") + b = &Announcement{IPv6: net.ParseIP("::2")} + assert.False(t, a.Equal(b), "should not match") + b = &Announcement{Transports: []string{"b", "c"}} + assert.False(t, a.Equal(b), "should not match") + b = &Announcement{Entry: []string{"b", "c"}} + assert.False(t, a.Equal(b), "should not match") + b = &Announcement{Exit: []string{"b", "c"}} + assert.False(t, a.Equal(b), "should not match") +} + +func TestStringify(t *testing.T) { + t.Parallel() + + assert.Equal(t, "", (&Hub{ID: "abcdefg", Info: &Announcement{}}).String()) + assert.Equal(t, "", (&Hub{ID: "abcdefgh", Info: &Announcement{}}).String()) + assert.Equal(t, "", (&Hub{ID: "abcdefghi", Info: &Announcement{}}).String()) + assert.Equal(t, "", (&Hub{ID: "abcdefghi", Info: &Announcement{Name: "Franz"}}).String()) + assert.Equal(t, "", (&Hub{ID: "abcdefghi", Info: &Announcement{Name: "AProbablyAutoGeneratedName"}}).String()) +} diff --git a/spn/hub/intel.go b/spn/hub/intel.go new file mode 100644 index 00000000..8bc505ed --- /dev/null +++ b/spn/hub/intel.go @@ -0,0 +1,191 @@ +package hub + +import ( + "errors" + "fmt" + "net" + + "github.com/ghodss/yaml" + + "github.com/safing/jess/lhash" + "github.com/safing/portmaster/service/profile/endpoints" +) + +// Intel holds a collection of various security related data collections on Hubs. +type Intel struct { + // BootstrapHubs is list of transports that also contain an IP and the Hub's ID. + BootstrapHubs []string + + // Hubs holds intel regarding specific Hubs. + Hubs map[string]*HubIntel + + // AdviseOnlyTrustedHubs advises to only use trusted Hubs regardless of intended purpose. + AdviseOnlyTrustedHubs bool + // AdviseOnlyTrustedHomeHubs advises to only use trusted Hubs for Home Hubs. + AdviseOnlyTrustedHomeHubs bool + // AdviseOnlyTrustedDestinationHubs advises to only use trusted Hubs for Destination Hubs. + AdviseOnlyTrustedDestinationHubs bool + + // Hub Advisories advise on the usage of Hubs and take the form of Endpoint Lists that match on both IPv4 and IPv6 addresses and their related data. + + // HubAdvisory always affects all Hubs. + HubAdvisory []string + // HomeHubAdvisory is only taken into account when selecting a Home Hub. + HomeHubAdvisory []string + // DestinationHubAdvisory is only taken into account when selecting a Destination Hub. + DestinationHubAdvisory []string + + // Regions defines regions to assist network optimization. + Regions []*RegionConfig + + // VirtualNetworks holds network configurations for virtual cloud networks. + VirtualNetworks []*VirtualNetworkConfig + + parsed *ParsedIntel +} + +// HubIntel holds Hub-related data. +type HubIntel struct { //nolint:golint + // Trusted specifies if the Hub is specially designated for more sensitive tasks, such as handling unencrypted traffic. + Trusted bool + + // Discontinued specifies if the Hub has been discontinued and should be marked as offline and removed. + Discontinued bool + + // VerifiedOwner holds the name of the verified owner / operator of the Hub. + VerifiedOwner string + + // Override is used to override certain Hub information. + Override *InfoOverride +} + +// RegionConfig holds the configuration of a region. +type RegionConfig struct { + // ID is the internal identifier of the region. + ID string + // Name is a human readable name of the region. + Name string + // MemberPolicy specifies a list for including members. + MemberPolicy []string + + // RegionalMinLanes specifies how many lanes other regions should build + // to this region. + RegionalMinLanes int + // RegionalMinLanesPerHub specifies how many lanes other regions should + // build to this region, per Hub in this region. + // This value will usually be below one. + RegionalMinLanesPerHub float64 + // RegionalMaxLanesOnHub specifies how many lanes from or to another region may be + // built on one Hub per region. + RegionalMaxLanesOnHub int + + // SatelliteMinLanes specifies how many lanes satellites (Hubs without + // region) should build to this region. + SatelliteMinLanes int + // SatelliteMinLanesPerHub specifies how many lanes satellites (Hubs without + // region) should build to this region, per Hub in this region. + // This value will usually be below one. + SatelliteMinLanesPerHub float64 + + // InternalMinLanesOnHub specifies how many lanes every Hub should create + // within the region at minimum. + InternalMinLanesOnHub int + // InternalMaxHops specifies the max hop constraint for internally optimizing + // the region. + InternalMaxHops int +} + +// VirtualNetworkConfig holds configuration of a virtual network that binds multiple Hubs together. +type VirtualNetworkConfig struct { + // Name is a human readable name of the virtual network. + Name string + // Force forces the use of the mapped IP addresses after the Hub's IPs have been verified. + Force bool + // Mapping maps Hub IDs to internal IP addresses. + Mapping map[string]net.IP +} + +// ParsedIntel holds a collection of parsed intel data. +type ParsedIntel struct { + // HubAdvisory always affects all Hubs. + HubAdvisory endpoints.Endpoints + + // HomeHubAdvisory is only taken into account when selecting a Home Hub. + HomeHubAdvisory endpoints.Endpoints + + // DestinationHubAdvisory is only taken into account when selecting a Destination Hub. + DestinationHubAdvisory endpoints.Endpoints +} + +// Parsed returns the collection of parsed intel data. +func (i *Intel) Parsed() *ParsedIntel { + return i.parsed +} + +// ParseIntel parses Hub intelligence data. +func ParseIntel(data []byte) (*Intel, error) { + // Load data into struct. + intel := &Intel{} + err := yaml.Unmarshal(data, intel) + if err != nil { + return nil, fmt.Errorf("failed to parse data: %w", err) + } + + // Parse all endpoint lists. + err = intel.ParseAdvisories() + if err != nil { + return nil, err + } + + return intel, nil +} + +// ParseAdvisories parses all advisory endpoint lists. +func (i *Intel) ParseAdvisories() (err error) { + i.parsed = &ParsedIntel{} + + i.parsed.HubAdvisory, err = endpoints.ParseEndpoints(i.HubAdvisory) + if err != nil { + return fmt.Errorf("failed to parse HubAdvisory list: %w", err) + } + + i.parsed.HomeHubAdvisory, err = endpoints.ParseEndpoints(i.HomeHubAdvisory) + if err != nil { + return fmt.Errorf("failed to parse HomeHubAdvisory list: %w", err) + } + + i.parsed.DestinationHubAdvisory, err = endpoints.ParseEndpoints(i.DestinationHubAdvisory) + if err != nil { + return fmt.Errorf("failed to parse DestinationHubAdvisory list: %w", err) + } + + return nil +} + +// ParseBootstrapHub parses a bootstrap hub. +func ParseBootstrapHub(bootstrapTransport string) (t *Transport, hubID string, hubIP net.IP, err error) { + // Parse transport and check Hub ID. + t, err = ParseTransport(bootstrapTransport) + if err != nil { + return nil, "", nil, fmt.Errorf("failed to parse transport: %w", err) + } + if t.Option == "" { + return nil, "", nil, errors.New("missing hub ID in URL fragment") + } + if _, err := lhash.FromBase58(t.Option); err != nil { + return nil, "", nil, fmt.Errorf("hub ID is invalid: %w", err) + } + + // Parse IP address from transport. + ip := net.ParseIP(t.Domain) + if ip == nil { + return nil, "", nil, errors.New("invalid IP address (domains are not supported for bootstrapping)") + } + + // Clean up transport for hub info. + id := t.Option + t.Domain = "" + t.Option = "" + + return t, id, ip, nil +} diff --git a/spn/hub/intel_override.go b/spn/hub/intel_override.go new file mode 100644 index 00000000..0fa7f29c --- /dev/null +++ b/spn/hub/intel_override.go @@ -0,0 +1,17 @@ +package hub + +import "github.com/safing/portmaster/service/intel/geoip" + +// InfoOverride holds data to overide hub info information. +type InfoOverride struct { + // ContinentCode overrides the continent code of the geoip data. + ContinentCode string + // CountryCode overrides the country code of the geoip data. + CountryCode string + // Coordinates overrides the geo coordinates code of the geoip data. + Coordinates *geoip.Coordinates + // ASN overrides the Autonomous System Number of the geoip data. + ASN uint + // ASOrg overrides the Autonomous System Organization of the geoip data. + ASOrg string +} diff --git a/spn/hub/measurements.go b/spn/hub/measurements.go new file mode 100644 index 00000000..135a67c9 --- /dev/null +++ b/spn/hub/measurements.go @@ -0,0 +1,231 @@ +package hub + +import ( + "sync" + "time" + + "github.com/tevino/abool" +) + +// MaxCalculatedCost specifies the max calculated cost to be used for an unknown high cost. +const MaxCalculatedCost = 1000000 + +// Measurements holds various measurements relating to a Hub. +// Fields may not be accessed directly. +type Measurements struct { + sync.Mutex + + // Latency designates the latency between these Hubs. + // It is specified in nanoseconds. + Latency time.Duration + // LatencyMeasuredAt holds when the latency was measured. + LatencyMeasuredAt time.Time + + // Capacity designates the available bandwidth between these Hubs. + // It is specified in bit/s. + Capacity int + // CapacityMeasuredAt holds when the capacity measurement expires. + CapacityMeasuredAt time.Time + + // CalculatedCost stores the calculated cost for direct access. + // It is not set automatically, but needs to be set when needed. + CalculatedCost float32 + + // GeoProximity stores an approximation of the geolocation proximity. + // The value is between 0 (other side of the world) and 100 (same location). + GeoProximity float32 + + // persisted holds whether the Measurements have been persisted to the + // database. + persisted *abool.AtomicBool +} + +// NewMeasurements returns a new measurements struct. +func NewMeasurements() *Measurements { + m := &Measurements{ + CalculatedCost: MaxCalculatedCost, // Push to back when sorting without data. + } + m.check() + return m +} + +// Copy returns a copy of the measurements. +func (m *Measurements) Copy() *Measurements { + copied := &Measurements{ + Latency: m.Latency, + LatencyMeasuredAt: m.LatencyMeasuredAt, + Capacity: m.Capacity, + CapacityMeasuredAt: m.CapacityMeasuredAt, + CalculatedCost: m.CalculatedCost, + } + copied.check() + return copied +} + +// Check checks if the Measurements are properly initialized and ready to use. +func (m *Measurements) check() { + if m == nil { + return + } + + m.Lock() + defer m.Unlock() + + if m.persisted == nil { + m.persisted = abool.NewBool(true) + } +} + +// IsPersisted return whether changes to the measurements have been persisted. +func (m *Measurements) IsPersisted() bool { + return m.persisted.IsSet() +} + +// Valid returns whether there is a valid value . +func (m *Measurements) Valid() bool { + m.Lock() + defer m.Unlock() + + switch { + case m.Latency == 0: + // Latency is not set. + case m.Capacity == 0: + // Capacity is not set. + case m.CalculatedCost == 0: + // CalculatedCost is not set. + case m.CalculatedCost == MaxCalculatedCost: + // CalculatedCost is set to static max value. + default: + return true + } + + return false +} + +// Expired returns whether any of the measurements has expired - calculated +// with the given TTL. +func (m *Measurements) Expired(ttl time.Duration) bool { + expiry := time.Now().Add(-ttl) + + m.Lock() + defer m.Unlock() + + switch { + case expiry.After(m.LatencyMeasuredAt): + return true + case expiry.After(m.CapacityMeasuredAt): + return true + default: + return false + } +} + +// SetLatency sets the latency to the given value. +func (m *Measurements) SetLatency(latency time.Duration) { + m.Lock() + defer m.Unlock() + + m.Latency = latency + m.LatencyMeasuredAt = time.Now() + m.persisted.UnSet() +} + +// GetLatency returns the latency and when it expires. +func (m *Measurements) GetLatency() (latency time.Duration, measuredAt time.Time) { + m.Lock() + defer m.Unlock() + + return m.Latency, m.LatencyMeasuredAt +} + +// SetCapacity sets the capacity to the given value. +// The capacity is measued in bit/s. +func (m *Measurements) SetCapacity(capacity int) { + m.Lock() + defer m.Unlock() + + m.Capacity = capacity + m.CapacityMeasuredAt = time.Now() + m.persisted.UnSet() +} + +// GetCapacity returns the capacity and when it expires. +// The capacity is measued in bit/s. +func (m *Measurements) GetCapacity() (capacity int, measuredAt time.Time) { + m.Lock() + defer m.Unlock() + + return m.Capacity, m.CapacityMeasuredAt +} + +// SetCalculatedCost sets the calculated cost to the given value. +// The calculated cost is not set automatically, but needs to be set when needed. +func (m *Measurements) SetCalculatedCost(cost float32) { + m.Lock() + defer m.Unlock() + + m.CalculatedCost = cost + m.persisted.UnSet() +} + +// GetCalculatedCost returns the calculated cost. +// The calculated cost is not set automatically, but needs to be set when needed. +func (m *Measurements) GetCalculatedCost() (cost float32) { + if m == nil { + return MaxCalculatedCost + } + + m.Lock() + defer m.Unlock() + + return m.CalculatedCost +} + +// SetGeoProximity sets the geolocation proximity to the given value. +func (m *Measurements) SetGeoProximity(geoProximity float32) { + m.Lock() + defer m.Unlock() + + m.GeoProximity = geoProximity + m.persisted.UnSet() +} + +// GetGeoProximity returns the geolocation proximity. +func (m *Measurements) GetGeoProximity() (geoProximity float32) { + if m == nil { + return 0 + } + + m.Lock() + defer m.Unlock() + + return m.GeoProximity +} + +var ( + measurementsRegistry = make(map[string]*Measurements) + measurementsRegistryLock sync.Mutex +) + +func getSharedMeasurements(hubID string, existing *Measurements) *Measurements { + measurementsRegistryLock.Lock() + defer measurementsRegistryLock.Unlock() + + // 1. Check registry and return shared measurements. + m, ok := measurementsRegistry[hubID] + if ok { + return m + } + + // 2. Use existing and make it shared, if available. + if existing != nil { + existing.check() + measurementsRegistry[hubID] = existing + return existing + } + + // 3. Create new measurements. + m = NewMeasurements() + measurementsRegistry[hubID] = m + return m +} diff --git a/spn/hub/status.go b/spn/hub/status.go new file mode 100644 index 00000000..0d5c4808 --- /dev/null +++ b/spn/hub/status.go @@ -0,0 +1,308 @@ +package hub + +import ( + "errors" + "fmt" + "sort" + "time" + + "golang.org/x/exp/slices" + + "github.com/safing/jess" +) + +// VersionOffline is a special version used to signify that the Hub has gone offline. +// This is depracated, please use FlagOffline instead. +const VersionOffline = "offline" + +// Status Flags. +const ( + // FlagNetError signifies that the Hub reports a network connectivity failure or impairment. + FlagNetError = "net-error" + + // FlagOffline signifies that the Hub has gone offline by itself. + FlagOffline = "offline" + + // FlagAllowUnencrypted signifies that the Hub is available to handle unencrypted connections. + FlagAllowUnencrypted = "allow-unencrypted" +) + +// Status is the message type used to update changing Hub Information. Changes are made automatically. +type Status struct { + Timestamp int64 `cbor:"t"` + + // Version holds the current software version of the Hub. + Version string `cbor:"v"` + + // Routing Information + Keys map[string]*Key `cbor:"k,omitempty" json:",omitempty"` // public keys (with type) + Lanes []*Lane `cbor:"c,omitempty" json:",omitempty"` // Connections to other Hubs. + + // Status Information + // Load describes max(CPU, Memory) in percent, averaged over at least 15 + // minutes. Load is published in fixed steps only. + Load int `cbor:"l,omitempty" json:",omitempty"` + + // Flags holds flags that signify special states. + Flags []string `cbor:"f,omitempty" json:",omitempty"` +} + +// Key represents a semi-ephemeral public key used for 0-RTT connection establishment. +type Key struct { + Scheme string + Key []byte + Expires int64 +} + +// Lane represents a connection to another Hub. +type Lane struct { + // ID is the Hub ID of the peer. + ID string + + // Capacity designates the available bandwidth between these Hubs. + // It is specified in bit/s. + Capacity int + + // Lateny designates the latency between these Hubs. + // It is specified in nanoseconds. + Latency time.Duration +} + +// Copy returns a deep copy of the Status. +func (s *Status) Copy() *Status { + newStatus := &Status{ + Timestamp: s.Timestamp, + Version: s.Version, + Lanes: slices.Clone(s.Lanes), + Load: s.Load, + Flags: slices.Clone(s.Flags), + } + // Copy map. + newStatus.Keys = make(map[string]*Key, len(s.Keys)) + for k, v := range s.Keys { + newStatus.Keys[k] = v + } + return newStatus +} + +// SelectSignet selects the public key to use for initiating connections to that Hub. +func (h *Hub) SelectSignet() *jess.Signet { + h.Lock() + defer h.Unlock() + + // Return no Signet if we don't have a Status. + if h.Status == nil { + return nil + } + + // TODO: select key based on preferred alg? + now := time.Now().Unix() + for id, key := range h.Status.Keys { + if now < key.Expires { + return &jess.Signet{ + ID: id, + Scheme: key.Scheme, + Key: key.Key, + Public: true, + } + } + } + + return nil +} + +// GetSignet returns the public key identified by the given ID from the Hub Status. +func (h *Hub) GetSignet(id string, recipient bool) (*jess.Signet, error) { + h.Lock() + defer h.Unlock() + + // check if public key is being requested + if !recipient { + return nil, jess.ErrSignetNotFound + } + // check if ID exists + key, ok := h.Status.Keys[id] + if !ok { + return nil, jess.ErrSignetNotFound + } + // transform and return + return &jess.Signet{ + ID: id, + Scheme: key.Scheme, + Key: key.Key, + Public: true, + }, nil +} + +// AddLane adds a new Lane to the Hub Status. +func (h *Hub) AddLane(newLane *Lane) error { + h.Lock() + defer h.Unlock() + + // validity check + if h.Status == nil { + return ErrMissingInfo + } + + // check if duplicate + for _, lane := range h.Status.Lanes { + if newLane.ID == lane.ID { + return errors.New("lane already exists") + } + } + + // add + h.Status.Lanes = append(h.Status.Lanes, newLane) + return nil +} + +// RemoveLane removes a Lane from the Hub Status. +func (h *Hub) RemoveLane(hubID string) error { + h.Lock() + defer h.Unlock() + + // validity check + if h.Status == nil { + return ErrMissingInfo + } + + for key, lane := range h.Status.Lanes { + if lane.ID == hubID { + h.Status.Lanes = append(h.Status.Lanes[:key], h.Status.Lanes[key+1:]...) + break + } + } + + return nil +} + +// GetLaneTo returns the lane to the given Hub, if it exists. +func (h *Hub) GetLaneTo(hubID string) *Lane { + h.Lock() + defer h.Unlock() + + // validity check + if h.Status == nil { + return nil + } + + for _, lane := range h.Status.Lanes { + if lane.ID == hubID { + return lane + } + } + + return nil +} + +// Equal returns whether the Lane is equal to the given one. +func (l *Lane) Equal(other *Lane) bool { + switch { + case l == nil || other == nil: + return false + case l.ID != other.ID: + return false + case l.Capacity != other.Capacity: + return false + case l.Latency != other.Latency: + return false + } + return true +} + +// validateFormatting check if all values conform to the basic format. +func (s *Status) validateFormatting() error { + // public keys + if len(s.Keys) > 255 { + return fmt.Errorf("field Keys with array/slice length of %d exceeds max length of %d", len(s.Keys), 255) + } + for keyID, key := range s.Keys { + if err := checkStringFormat("Keys#ID", keyID, 255); err != nil { + return err + } + if err := checkStringFormat("Keys.Scheme", key.Scheme, 255); err != nil { + return err + } + if err := checkByteSliceFormat("Keys.Key", key.Key, 1024); err != nil { + return err + } + } + + // connections + if len(s.Lanes) > 255 { + return fmt.Errorf("field Lanes with array/slice length of %d exceeds max length of %d", len(s.Lanes), 255) + } + for _, lanes := range s.Lanes { + if err := checkStringFormat("Lanes.ID", lanes.ID, 255); err != nil { + return err + } + } + + // Flags + if err := checkStringSliceFormat("Flags", s.Flags, 255, 255); err != nil { + return err + } + + return nil +} + +func (l *Lane) String() string { + return fmt.Sprintf("<%s cap=%d lat=%d>", l.ID, l.Capacity, l.Latency) +} + +// LanesEqual returns whether the given []*Lane are equal. +func LanesEqual(a, b []*Lane) bool { + if len(a) != len(b) { + return false + } + + for i, l := range a { + if !l.Equal(b[i]) { + return false + } + } + + return true +} + +type lanes []*Lane + +func (l lanes) Len() int { return len(l) } +func (l lanes) Swap(i, j int) { l[i], l[j] = l[j], l[i] } +func (l lanes) Less(i, j int) bool { return l[i].ID < l[j].ID } + +// SortLanes sorts a slice of Lanes. +func SortLanes(l []*Lane) { + sort.Sort(lanes(l)) +} + +// HasFlag returns whether the Status has the given flag set. +func (s *Status) HasFlag(flagName string) bool { + return slices.Contains[[]string, string](s.Flags, flagName) +} + +// FlagsEqual returns whether the given status flags are equal. +func FlagsEqual(a, b []string) bool { + // Cannot be equal if lengths are different. + if len(a) != len(b) { + return false + } + + // If both are empty, they are equal. + if len(a) == 0 { + return true + } + + // Make sure flags are sorted before comparing values. + sort.Strings(a) + sort.Strings(b) + + // Compare values. + for i, v := range a { + if v != b[i] { + return false + } + } + + return true +} diff --git a/spn/hub/transport.go b/spn/hub/transport.go new file mode 100644 index 00000000..aa8f3bf9 --- /dev/null +++ b/spn/hub/transport.go @@ -0,0 +1,152 @@ +package hub + +import ( + "errors" + "fmt" + "net/url" + "strconv" + "strings" + + "golang.org/x/exp/slices" +) + +// Examples: +// "spn:17", +// "smtp:25", +// "smtp:587", +// "imap:143", +// "http:80", +// "http://example.com:80/example", // HTTP (based): use full path for request +// "https:443", +// "ws:80", +// "wss://example.com:443/spn", + +// Transport represents a "endpoint" that others can connect to. This allows for use of different protocols, ports and infrastructure integration. +type Transport struct { + Protocol string + Domain string + Port uint16 + Path string + Option string +} + +// ParseTransports returns a list of parsed transports and errors from parsing +// the given definitions. +func ParseTransports(definitions []string) (transports []*Transport, errs []error) { + transports = make([]*Transport, 0, len(definitions)) + for _, definition := range definitions { + parsed, err := ParseTransport(definition) + if err != nil { + errs = append(errs, fmt.Errorf( + "unknown or invalid transport %q: %w", definition, err, + )) + } else { + transports = append(transports, parsed) + } + } + + SortTransports(transports) + return transports, errs +} + +// ParseTransport parses a transport definition. +func ParseTransport(definition string) (*Transport, error) { + u, err := url.Parse(definition) + if err != nil { + return nil, err + } + + // check for invalid parts + if u.User != nil { + return nil, errors.New("user/pass is not allowed") + } + + // put into transport + t := &Transport{ + Protocol: u.Scheme, + Domain: u.Hostname(), + Path: u.RequestURI(), + Option: u.Fragment, + } + + // parse port + portData := u.Port() + if portData == "" { + // no port available - it might be in u.Opaque, which holds both the port and possibly a path + portData = strings.SplitN(u.Opaque, "/", 2)[0] // get port + t.Path = strings.TrimPrefix(t.Path, portData) // trim port from path + // check again for port + if portData == "" { + return nil, errors.New("missing port") + } + } + port, err := strconv.ParseUint(portData, 10, 16) + if err != nil { + return nil, errors.New("invalid port") + } + t.Port = uint16(port) + + // check port + if t.Port == 0 { + return nil, errors.New("invalid port") + } + + // remove root paths + if t.Path == "/" { + t.Path = "" + } + + // check for protocol + if t.Protocol == "" { + return nil, errors.New("missing scheme/protocol") + } + + return t, nil +} + +// String returns the definition form of the transport. +func (t *Transport) String() string { + switch { + case t.Option != "": + return fmt.Sprintf("%s://%s:%d%s#%s", t.Protocol, t.Domain, t.Port, t.Path, t.Option) + case t.Domain != "": + return fmt.Sprintf("%s://%s:%d%s", t.Protocol, t.Domain, t.Port, t.Path) + default: + return fmt.Sprintf("%s:%d%s", t.Protocol, t.Port, t.Path) + } +} + +// SortTransports sorts the transports to emphasize certain protocols, but +// otherwise leaves the order intact. +func SortTransports(ts []*Transport) { + slices.SortStableFunc[[]*Transport, *Transport](ts, func(a, b *Transport) int { + aOrder := a.protocolOrder() + bOrder := b.protocolOrder() + + switch { + case aOrder != bOrder: + return aOrder - bOrder + // case a.Port != b.Port: + // return int(a.Port) - int(b.Port) + // case a.Domain != b.Domain: + // return strings.Compare(a.Domain, b.Domain) + // case a.Path != b.Path: + // return strings.Compare(a.Path, b.Path) + // case a.Option != b.Option: + // return strings.Compare(a.Option, b.Option) + default: + return 0 + } + }) +} + +func (t *Transport) protocolOrder() int { + switch t.Protocol { + case "http": + return 1 + case "spn": + return 2 + default: + return 100 + } +} diff --git a/spn/hub/transport_test.go b/spn/hub/transport_test.go new file mode 100644 index 00000000..c885fcfa --- /dev/null +++ b/spn/hub/transport_test.go @@ -0,0 +1,147 @@ +package hub + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func parseT(t *testing.T, definition string) *Transport { + t.Helper() + + tr, err := ParseTransport(definition) + if err != nil { + t.Fatal(err) + return nil + } + return tr +} + +func parseTError(definition string) error { + _, err := ParseTransport(definition) + return err +} + +func TestTransportParsing(t *testing.T) { + t.Parallel() + + // test parsing + + assert.Equal(t, &Transport{ + Protocol: "spn", + Port: 17, + }, parseT(t, "spn:17"), "should match") + + assert.Equal(t, &Transport{ + Protocol: "smtp", + Port: 25, + }, parseT(t, "smtp:25"), "should match") + + assert.Equal(t, &Transport{ + Protocol: "smtp", + Port: 25, + }, parseT(t, "smtp://:25"), "should match") + + assert.Equal(t, &Transport{ + Protocol: "smtp", + Port: 587, + }, parseT(t, "smtp:587"), "should match") + + assert.Equal(t, &Transport{ + Protocol: "imap", + Port: 143, + }, parseT(t, "imap:143"), "should match") + + assert.Equal(t, &Transport{ + Protocol: "http", + Port: 80, + }, parseT(t, "http:80"), "should match") + + assert.Equal(t, &Transport{ + Protocol: "http", + Domain: "example.com", + Port: 80, + }, parseT(t, "http://example.com:80"), "should match") + + assert.Equal(t, &Transport{ + Protocol: "https", + Port: 443, + }, parseT(t, "https:443"), "should match") + + assert.Equal(t, &Transport{ + Protocol: "ws", + Port: 80, + }, parseT(t, "ws:80"), "should match") + + assert.Equal(t, &Transport{ + Protocol: "wss", + Domain: "example.com", + Port: 443, + Path: "/spn", + }, parseT(t, "wss://example.com:443/spn"), "should match") + + assert.Equal(t, &Transport{ + Protocol: "http", + Domain: "example.com", + Port: 80, + }, parseT(t, "http://example.com:80"), "should match") + + assert.Equal(t, &Transport{ + Protocol: "http", + Domain: "example.com", + Port: 80, + Path: "/test%20test", + }, parseT(t, "http://example.com:80/test test"), "should match") + + assert.Equal(t, &Transport{ + Protocol: "http", + Domain: "example.com", + Port: 80, + Path: "/test%20test", + }, parseT(t, "http://example.com:80/test%20test"), "should match") + + assert.Equal(t, &Transport{ + Protocol: "http", + Domain: "example.com", + Port: 80, + Path: "/test?key=value", + }, parseT(t, "http://example.com:80/test?key=value"), "should match") + + // test parsing and formatting + + assert.Equal(t, "spn:17", + parseT(t, "spn:17").String(), "should match") + assert.Equal(t, "smtp:25", + parseT(t, "smtp:25").String(), "should match") + assert.Equal(t, "smtp:25", + parseT(t, "smtp://:25").String(), "should match") + assert.Equal(t, "smtp:587", + parseT(t, "smtp:587").String(), "should match") + assert.Equal(t, "imap:143", + parseT(t, "imap:143").String(), "should match") + assert.Equal(t, "http:80", + parseT(t, "http:80").String(), "should match") + assert.Equal(t, "http://example.com:80", + parseT(t, "http://example.com:80").String(), "should match") + assert.Equal(t, "https:443", + parseT(t, "https:443").String(), "should match") + assert.Equal(t, "ws:80", + parseT(t, "ws:80").String(), "should match") + assert.Equal(t, "wss://example.com:443/spn", + parseT(t, "wss://example.com:443/spn").String(), "should match") + assert.Equal(t, "http://example.com:80", + parseT(t, "http://example.com:80").String(), "should match") + assert.Equal(t, "http://example.com:80/test%20test", + parseT(t, "http://example.com:80/test test").String(), "should match") + assert.Equal(t, "http://example.com:80/test%20test", + parseT(t, "http://example.com:80/test%20test").String(), "should match") + assert.Equal(t, "http://example.com:80/test?key=value", + parseT(t, "http://example.com:80/test?key=value").String(), "should match") + + // test invalid + + assert.NotEqual(t, parseTError("spn"), nil, "should fail") + assert.NotEqual(t, parseTError("spn:"), nil, "should fail") + assert.NotEqual(t, parseTError("spn:0"), nil, "should fail") + assert.NotEqual(t, parseTError("spn:65536"), nil, "should fail") +} diff --git a/spn/hub/truststores.go b/spn/hub/truststores.go new file mode 100644 index 00000000..8f06a55d --- /dev/null +++ b/spn/hub/truststores.go @@ -0,0 +1,17 @@ +package hub + +import "github.com/safing/jess" + +// SingleTrustStore is a simple truststore that always returns the same Signet. +type SingleTrustStore struct { + Signet *jess.Signet +} + +// GetSignet implements the truststore interface. +func (ts *SingleTrustStore) GetSignet(id string, recipient bool) (*jess.Signet, error) { + if ts.Signet.ID != id || recipient != ts.Signet.Public { + return nil, jess.ErrSignetNotFound + } + + return ts.Signet, nil +} diff --git a/spn/hub/update.go b/spn/hub/update.go new file mode 100644 index 00000000..e2009db4 --- /dev/null +++ b/spn/hub/update.go @@ -0,0 +1,524 @@ +package hub + +import ( + "errors" + "fmt" + "time" + + "github.com/safing/jess" + "github.com/safing/jess/lhash" + "github.com/safing/portbase/container" + "github.com/safing/portbase/database" + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/service/network/netutils" +) + +var ( + // hubMsgRequirements defines which security attributes message need to have. + hubMsgRequirements = jess.NewRequirements(). + Remove(jess.RecipientAuthentication). // Recipient don't need a private key. + Remove(jess.Confidentiality). // Message contents are out in the open. + Remove(jess.Integrity) // Only applies to decryption. + // SenderAuthentication provides pre-decryption integrity. That is all we need. + + clockSkewTolerance = 1 * time.Hour +) + +// SignHubMsg signs the given serialized hub msg with the given configuration. +func SignHubMsg(msg []byte, env *jess.Envelope, enableTofu bool) ([]byte, error) { + // start session from envelope + session, err := env.Correspondence(nil) + if err != nil { + return nil, fmt.Errorf("failed to initiate signing session: %w", err) + } + // sign the data + letter, err := session.Close(msg) + if err != nil { + return nil, fmt.Errorf("failed to sign msg: %w", err) + } + + if enableTofu { + // smuggle the public key + // letter.Keys is usually only used for key exchanges and encapsulation + // neither is used when signing, so we can use letter.Keys to transport public keys + for _, sender := range env.Senders { + // get public key + public, err := sender.AsRecipient() + if err != nil { + return nil, fmt.Errorf("failed to get public key of %s: %w", sender.ID, err) + } + // serialize key + err = public.StoreKey() + if err != nil { + return nil, fmt.Errorf("failed to serialize public key %s: %w", sender.ID, err) + } + // add to keys + letter.Keys = append(letter.Keys, &jess.Seal{ + Value: public.Key, + }) + } + } + + // pack + data, err := letter.ToDSD(dsd.JSON) + if err != nil { + return nil, err + } + + return data, nil +} + +// OpenHubMsg opens a signed hub msg and verifies the signature using the +// provided hub or the local database. If TOFU is enabled, the signature is +// always accepted, if valid. +func OpenHubMsg(hub *Hub, data []byte, mapName string, tofu bool) (msg []byte, sendingHub *Hub, known bool, err error) { + letter, err := jess.LetterFromDSD(data) + if err != nil { + return nil, nil, false, fmt.Errorf("malformed letter: %w", err) + } + + // check signatures + var seal *jess.Seal + switch len(letter.Signatures) { + case 0: + return nil, nil, false, errors.New("missing signature") + case 1: + seal = letter.Signatures[0] + default: + return nil, nil, false, fmt.Errorf("too many signatures (%d)", len(letter.Signatures)) + } + + // check signature signer ID + if seal.ID == "" { + return nil, nil, false, errors.New("signature is missing signer ID") + } + + // get hub for public key + if hub == nil { + hub, err = GetHub(mapName, seal.ID) + if err != nil { + if !errors.Is(err, database.ErrNotFound) { + return nil, nil, false, fmt.Errorf("failed to get existing hub %s: %w", seal.ID, err) + } + hub = nil + } else { + known = true + } + } else { + known = true + } + + var truststore jess.TrustStore + if hub != nil && hub.PublicKey != nil { // bootstrap entries will not have a public key + // check ID integrity + if hub.ID != seal.ID { + return nil, hub, known, fmt.Errorf("ID mismatch with hub msg ID %s and hub ID %s", seal.ID, hub.ID) + } + if !verifyHubID(seal.ID, hub.PublicKey.Scheme, hub.PublicKey.Key) { + return nil, hub, known, fmt.Errorf("ID integrity of %s violated with existing key", seal.ID) + } + } else { + if !tofu { + return nil, nil, false, fmt.Errorf("hub msg ID %s unknown (missing announcement)", seal.ID) + } + + // trust on first use, extract key from keys + // TODO: Test if works without TOFU. + + // get key + var pubkey *jess.Seal + switch len(letter.Keys) { + case 0: + return nil, nil, false, fmt.Errorf("missing key for TOFU of %s", seal.ID) + case 1: + pubkey = letter.Keys[0] + default: + return nil, nil, false, fmt.Errorf("too many keys (%d) for TOFU of %s", len(letter.Keys), seal.ID) + } + + // check ID integrity + if !verifyHubID(seal.ID, seal.Scheme, pubkey.Value) { + return nil, nil, false, fmt.Errorf("ID integrity of %s violated with new key", seal.ID) + } + + hub = &Hub{ + ID: seal.ID, + Map: mapName, + PublicKey: &jess.Signet{ + ID: seal.ID, + Scheme: seal.Scheme, + Key: pubkey.Value, + Public: true, + }, + } + err = hub.PublicKey.LoadKey() + if err != nil { + return nil, nil, false, err + } + } + + // create trust store + truststore = &SingleTrustStore{hub.PublicKey} + + // remove keys from letter, as they are only used to transfer the public key + letter.Keys = nil + + // check signature + err = letter.Verify(hubMsgRequirements, truststore) + if err != nil { + return nil, nil, false, err + } + + return letter.Data, hub, known, nil +} + +// Export exports the announcement with the given signature configuration. +func (a *Announcement) Export(env *jess.Envelope) ([]byte, error) { + // pack + msg, err := dsd.Dump(a, dsd.JSON) + if err != nil { + return nil, fmt.Errorf("failed to pack announcement: %w", err) + } + + return SignHubMsg(msg, env, true) +} + +// ApplyAnnouncement applies the announcement to the Hub if it passes all the +// checks. If no Hub is provided, it is loaded from the database or created. +func ApplyAnnouncement(existingHub *Hub, data []byte, mapName string, scope Scope, selfcheck bool) (hub *Hub, known, changed bool, err error) { + // Set valid/invalid status based on the return error. + var announcement *Announcement + defer func() { + if hub != nil { + if err != nil && !errors.Is(err, ErrOldData) { + hub.InvalidInfo = true + } else { + hub.InvalidInfo = false + } + } + }() + + // open and verify + var msg []byte + msg, hub, known, err = OpenHubMsg(existingHub, data, mapName, true) + + // Lock hub if we have one. + if hub != nil && !selfcheck { + hub.Lock() + defer hub.Unlock() + } + + // Check if there was an error with the Hub msg. + if err != nil { + return //nolint:nakedret + } + + // parse + announcement = &Announcement{} + _, err = dsd.Load(msg, announcement) + if err != nil { + return //nolint:nakedret + } + + // integrity check + + // `hub.ID` is taken from the first ever received announcement message. + // `announcement.ID` is additionally present in the message as we need + // a signed version of the ID to mitigate fake IDs. + // Fake IDs are possible because the hash algorithm of the ID is dynamic. + if hub.ID != announcement.ID { + err = fmt.Errorf("announcement ID %q mismatches hub ID %q", announcement.ID, hub.ID) + return //nolint:nakedret + } + + // version check + if hub.Info != nil { + // check if we already have this version + switch { + case announcement.Timestamp == hub.Info.Timestamp && !selfcheck: + // The new copy is not saved, as we expect the versions to be identical. + // Also, the new version has not been validated at this point. + return //nolint:nakedret + case announcement.Timestamp < hub.Info.Timestamp: + // Received an old version, do not update. + err = fmt.Errorf( + "%wannouncement from %s @ %s is older than current status @ %s", + ErrOldData, hub.StringWithoutLocking(), time.Unix(announcement.Timestamp, 0), time.Unix(hub.Info.Timestamp, 0), + ) + return //nolint:nakedret + } + } + + // We received a new version. + changed = true + + // Update timestamp here already in case validation fails. + if hub.Info != nil { + hub.Info.Timestamp = announcement.Timestamp + } + + // Validate the announcement. + err = hub.validateAnnouncement(announcement, scope) + if err != nil { + if selfcheck || hub.FirstSeen.IsZero() { + err = fmt.Errorf("failed to validate announcement of %s: %w", hub.StringWithoutLocking(), err) + return //nolint:nakedret + } + + log.Warningf("spn/hub: received an invalid announcement of %s: %s", hub.StringWithoutLocking(), err) + // If a previously fully validated Hub publishes an update that breaks it, a + // soft-fail will accept the faulty changes, but mark is as invalid and + // forward it to neighbors. This way the invalid update is propagated through + // the network and all nodes will mark it as invalid an thus ingore the Hub + // until the issue is fixed. + } + + // Only save announcement if it is valid. + if err == nil { + hub.Info = announcement + } + // Set FirstSeen timestamp when we see this Hub for the first time. + if hub.FirstSeen.IsZero() { + hub.FirstSeen = time.Now().UTC() + } + + return //nolint:nakedret +} + +func (h *Hub) validateAnnouncement(announcement *Announcement, scope Scope) error { + // value formatting + if err := announcement.validateFormatting(); err != nil { + return err + } + // check parsables + if err := announcement.prepare(true); err != nil { + return fmt.Errorf("failed to prepare announcement: %w", err) + } + + // check timestamp + if announcement.Timestamp > time.Now().Add(clockSkewTolerance).Unix() { + return fmt.Errorf( + "announcement from %s @ %s is from the future", + announcement.ID, + time.Unix(announcement.Timestamp, 0), + ) + } + + // check for illegal IP address changes + if h.Info != nil { + switch { + case h.Info.IPv4 != nil && announcement.IPv4 == nil: + h.VerifiedIPs = false + return errors.New("previously announced IPv4 address missing") + case h.Info.IPv4 != nil && !announcement.IPv4.Equal(h.Info.IPv4): + h.VerifiedIPs = false + return errors.New("IPv4 address changed") + case h.Info.IPv6 != nil && announcement.IPv6 == nil: + h.VerifiedIPs = false + return errors.New("previously announced IPv6 address missing") + case h.Info.IPv6 != nil && !announcement.IPv6.Equal(h.Info.IPv6): + h.VerifiedIPs = false + return errors.New("IPv6 address changed") + } + } + + // validate IP scopes + if announcement.IPv4 != nil { + ipScope := netutils.GetIPScope(announcement.IPv4) + switch { + case scope == ScopeLocal && !ipScope.IsLAN(): + return errors.New("IPv4 scope violation: outside of local scope") + case scope == ScopePublic && !ipScope.IsGlobal(): + return errors.New("IPv4 scope violation: outside of global scope") + } + // Reset IP verification flag if IPv4 was added. + if h.Info == nil || h.Info.IPv4 == nil { + h.VerifiedIPs = false + } + } + if announcement.IPv6 != nil { + ipScope := netutils.GetIPScope(announcement.IPv6) + switch { + case scope == ScopeLocal && !ipScope.IsLAN(): + return errors.New("IPv6 scope violation: outside of local scope") + case scope == ScopePublic && !ipScope.IsGlobal(): + return errors.New("IPv6 scope violation: outside of global scope") + } + // Reset IP verification flag if IPv6 was added. + if h.Info == nil || h.Info.IPv6 == nil { + h.VerifiedIPs = false + } + } + + return nil +} + +// Export exports the status with the given signature configuration. +func (s *Status) Export(env *jess.Envelope) ([]byte, error) { + // pack + msg, err := dsd.Dump(s, dsd.JSON) + if err != nil { + return nil, fmt.Errorf("failed to pack status: %w", err) + } + + return SignHubMsg(msg, env, false) +} + +// ApplyStatus applies a status update if it passes all the checks. +func ApplyStatus(existingHub *Hub, data []byte, mapName string, scope Scope, selfcheck bool) (hub *Hub, known, changed bool, err error) { + // Set valid/invalid status based on the return error. + defer func() { + if hub != nil { + if err != nil && !errors.Is(err, ErrOldData) { + hub.InvalidStatus = true + } else { + hub.InvalidStatus = false + } + } + }() + + // open and verify + var msg []byte + msg, hub, known, err = OpenHubMsg(existingHub, data, mapName, false) + + // Lock hub if we have one. + if hub != nil && !selfcheck { + hub.Lock() + defer hub.Unlock() + } + + // Check if there was an error with the Hub msg. + if err != nil { + return //nolint:nakedret + } + + // parse + status := &Status{} + _, err = dsd.Load(msg, status) + if err != nil { + return //nolint:nakedret + } + + // version check + if hub.Status != nil { + // check if we already have this version + switch { + case status.Timestamp == hub.Status.Timestamp && !selfcheck: + // The new copy is not saved, as we expect the versions to be identical. + // Also, the new version has not been validated at this point. + return //nolint:nakedret + case status.Timestamp < hub.Status.Timestamp: + // Received an old version, do not update. + err = fmt.Errorf( + "%wstatus from %s @ %s is older than current status @ %s", + ErrOldData, hub.StringWithoutLocking(), time.Unix(status.Timestamp, 0), time.Unix(hub.Status.Timestamp, 0), + ) + return //nolint:nakedret + } + } + + // We received a new version. + changed = true + + // Update timestamp here already in case validation fails. + if hub.Status != nil { + hub.Status.Timestamp = status.Timestamp + } + + // Validate the status. + err = hub.validateStatus(status) + if err != nil { + if selfcheck { + err = fmt.Errorf("failed to validate status of %s: %w", hub.StringWithoutLocking(), err) + return //nolint:nakedret + } + + log.Warningf("spn/hub: received an invalid status of %s: %s", hub.StringWithoutLocking(), err) + // If a previously fully validated Hub publishes an update that breaks it, a + // soft-fail will accept the faulty changes, but mark is as invalid and + // forward it to neighbors. This way the invalid update is propagated through + // the network and all nodes will mark it as invalid an thus ingore the Hub + // until the issue is fixed. + } + + // Only save status if it is valid, else mark it as invalid. + if err == nil { + hub.Status = status + } + + return //nolint:nakedret +} + +func (h *Hub) validateStatus(status *Status) error { + // value formatting + if err := status.validateFormatting(); err != nil { + return err + } + + // check timestamp + if status.Timestamp > time.Now().Add(clockSkewTolerance).Unix() { + return fmt.Errorf( + "status from %s @ %s is from the future", + h.ID, + time.Unix(status.Timestamp, 0), + ) + } + + // TODO: validate status.Keys + + return nil +} + +// CreateHubSignet creates a signet with the correct ID for usage as a Hub Identity. +func CreateHubSignet(toolID string, securityLevel int) (private, public *jess.Signet, err error) { + private, err = jess.GenerateSignet(toolID, securityLevel) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate key: %w", err) + } + err = private.StoreKey() + if err != nil { + return nil, nil, fmt.Errorf("failed to store private key: %w", err) + } + + // get public key for creating the Hub ID + public, err = private.AsRecipient() + if err != nil { + return nil, nil, fmt.Errorf("failed to get public key: %w", err) + } + err = public.StoreKey() + if err != nil { + return nil, nil, fmt.Errorf("failed to store public key: %w", err) + } + + // assign IDs + private.ID = createHubID(public.Scheme, public.Key) + public.ID = private.ID + + return private, public, nil +} + +func createHubID(scheme string, pubkey []byte) string { + // compile scheme and public key + c := container.New() + c.AppendAsBlock([]byte(scheme)) + c.AppendAsBlock(pubkey) + + return lhash.Digest(lhash.BLAKE2b_256, c.CompileData()).Base58() +} + +func verifyHubID(id string, scheme string, pubkey []byte) (ok bool) { + // load labeled hash from ID + labeledHash, err := lhash.FromBase58(id) + if err != nil { + return false + } + + // compile scheme and public key + c := container.New() + c.AppendAsBlock([]byte(scheme)) + c.AppendAsBlock(pubkey) + + // check if it matches + return labeledHash.MatchesData(c.CompileData()) +} diff --git a/spn/hub/update_test.go b/spn/hub/update_test.go new file mode 100644 index 00000000..982f3206 --- /dev/null +++ b/spn/hub/update_test.go @@ -0,0 +1,70 @@ +package hub + +import ( + "fmt" + "testing" + + "github.com/safing/jess" + "github.com/safing/portbase/formats/dsd" +) + +func TestHubUpdate(t *testing.T) { + t.Parallel() + + // message signing + + testData := []byte{0} + + s1, err := jess.GenerateSignet("Ed25519", 0) + if err != nil { + t.Fatal(err) + } + err = s1.StoreKey() + if err != nil { + t.Fatal(err) + } + fmt.Printf("s1: %+v\n", s1) + + s1e, err := s1.AsRecipient() + if err != nil { + t.Fatal(err) + } + err = s1e.StoreKey() + if err != nil { + t.Fatal(err) + } + s1e.ID = createHubID(s1e.Scheme, s1e.Key) + s1.ID = s1e.ID + + t.Logf("generated hub ID: %s", s1.ID) + + env := jess.NewUnconfiguredEnvelope() + env.SuiteID = jess.SuiteSignV1 + env.Senders = []*jess.Signet{s1} + + s, err := env.Correspondence(nil) + if err != nil { + t.Fatal(err) + } + letter, err := s.Close(testData) + if err != nil { + t.Fatal(err) + } + + // smuggle the key + letter.Keys = append(letter.Keys, &jess.Seal{ + Value: s1e.Key, + }) + t.Logf("letter with smuggled key: %+v", letter) + + // pack + data, err := letter.ToDSD(dsd.JSON) + if err != nil { + t.Fatal(err) + } + + _, _, _, err = OpenHubMsg(nil, data, "test", true) //nolint:dogsled + if err != nil { + t.Fatal(err) + } +} diff --git a/spn/navigator/api.go b/spn/navigator/api.go new file mode 100644 index 00000000..832d1126 --- /dev/null +++ b/spn/navigator/api.go @@ -0,0 +1,672 @@ +package navigator + +import ( + "bytes" + "errors" + "fmt" + "math" + "net/http" + "sort" + "strconv" + "strings" + "sync" + "text/tabwriter" + "time" + + "github.com/awalterschulze/gographviz" + + "github.com/safing/portbase/api" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/docks" + "github.com/safing/portmaster/spn/hub" +) + +var ( + apiMapsLock sync.Mutex + apiMaps = make(map[string]*Map) +) + +func addMapToAPI(m *Map) { + apiMapsLock.Lock() + defer apiMapsLock.Unlock() + + apiMaps[m.Name] = m +} + +func getMapForAPI(name string) (m *Map, ok bool) { + apiMapsLock.Lock() + defer apiMapsLock.Unlock() + + m, ok = apiMaps[name] + return +} + +func removeMapFromAPI(name string) { + apiMapsLock.Lock() + defer apiMapsLock.Unlock() + + delete(apiMaps, name) +} + +func registerAPIEndpoints() error { + if err := api.RegisterEndpoint(api.Endpoint{ + Path: `spn/map/{map:[A-Za-z0-9]{1,255}}/pins`, + Read: api.PermitUser, + BelongsTo: module, + StructFunc: handleMapPinsRequest, + Name: "Get SPN map pins", + Description: "Returns a list of pins on the map.", + }); err != nil { + return err + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Path: `spn/map/{map:[A-Za-z0-9]{1,255}}/intel/update`, + Write: api.PermitSelf, + BelongsTo: module, + ActionFunc: handleIntelUpdateRequest, + Name: "Update map intelligence.", + Description: "Updates the intel data of the map.", + }); err != nil { + return err + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Path: `spn/map/{map:[A-Za-z0-9]{1,255}}/optimization`, + Read: api.PermitUser, + BelongsTo: module, + StructFunc: handleMapOptimizationRequest, + Name: "Get SPN map optimization", + Description: "Returns the calculated optimization for the map.", + }); err != nil { + return err + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Path: `spn/map/{map:[A-Za-z0-9]{1,255}}/optimization/table`, + Read: api.PermitUser, + BelongsTo: module, + DataFunc: handleMapOptimizationTableRequest, + Name: "Get SPN map optimization as a table", + Description: "Returns the calculated optimization for the map as a table.", + }); err != nil { + return err + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Path: `spn/map/{map:[A-Za-z0-9]{1,255}}/measurements`, + Read: api.PermitUser, + BelongsTo: module, + StructFunc: handleMapMeasurementsRequest, + Name: "Get SPN map measurements", + Description: "Returns the measurements of the map.", + }); err != nil { + return err + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Path: `spn/map/{map:[A-Za-z0-9]{1,255}}/measurements/table`, + MimeType: api.MimeTypeText, + Read: api.PermitUser, + BelongsTo: module, + DataFunc: handleMapMeasurementsTableRequest, + Name: "Get SPN map measurements as a table", + Description: "Returns the measurements of the map as a table.", + }); err != nil { + return err + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Path: `spn/map/{map:[A-Za-z0-9]{1,255}}/graph{format:\.[a-z]{2,4}}`, + Read: api.PermitUser, + BelongsTo: module, + HandlerFunc: handleMapGraphRequest, + Name: "Get SPN map graph", + Description: "Returns a graph of the given SPN map.", + Parameters: []api.Parameter{ + { + Method: http.MethodGet, + Field: "map (in path)", + Value: "name of map", + Description: "Specify the map you want to get the map for. The main map is called `main`.", + }, + { + Method: http.MethodGet, + Field: "format (in path)", + Value: "file type", + Description: "Specify the format you want to get the map in. Available values: `dot`, `html`. Please note that the html format is only available in development mode.", + }, + }, + }); err != nil { + return err + } + + // Register API endpoints from other files. + if err := registerRouteAPIEndpoints(); err != nil { + return err + } + + return nil +} + +func handleMapPinsRequest(ar *api.Request) (i interface{}, err error) { + // Get map. + m, ok := getMapForAPI(ar.URLVars["map"]) + if !ok { + return nil, errors.New("map not found") + } + + // Export all pins. + sortedPins := m.sortedPins(true) + exportedPins := make([]*PinExport, len(sortedPins)) + for key, pin := range sortedPins { + exportedPins[key] = pin.Export() + } + + return exportedPins, nil +} + +func handleIntelUpdateRequest(ar *api.Request) (msg string, err error) { + // Get map. + m, ok := getMapForAPI(ar.URLVars["map"]) + if !ok { + return "", errors.New("map not found") + } + + // Parse new intel data. + newIntel, err := hub.ParseIntel(ar.InputData) + if err != nil { + return "", fmt.Errorf("failed to parse intel data: %w", err) + } + + // Apply intel data. + err = m.UpdateIntel(newIntel, cfgOptionTrustNodeNodes()) + if err != nil { + return "", fmt.Errorf("failed to apply intel data: %w", err) + } + + return "successfully applied given intel data", nil +} + +func handleMapOptimizationRequest(ar *api.Request) (i interface{}, err error) { + // Get map. + m, ok := getMapForAPI(ar.URLVars["map"]) + if !ok { + return nil, errors.New("map not found") + } + + return m.Optimize(nil) +} + +func handleMapOptimizationTableRequest(ar *api.Request) (data []byte, err error) { + // Get map. + m, ok := getMapForAPI(ar.URLVars["map"]) + if !ok { + return nil, errors.New("map not found") + } + + // Get optimization result. + result, err := m.Optimize(nil) + if err != nil { + return nil, err + } + + // Read lock map, as we access pins. + m.RLock() + defer m.RUnlock() + + // Get cranes for additional metadata. + assignedCranes := docks.GetAllAssignedCranes() + + // Write metadata. + buf := bytes.NewBuffer(nil) + buf.WriteString("Optimization:\n") + fmt.Fprintf(buf, "Purpose: %s\n", result.Purpose) + if len(result.Approach) == 1 { + fmt.Fprintf(buf, "Approach: %s\n", result.Approach[0]) + } else if len(result.Approach) > 1 { + buf.WriteString("Approach:\n") + for _, approach := range result.Approach { + fmt.Fprintf(buf, " - %s\n", approach) + } + } + fmt.Fprintf(buf, "MaxConnect: %d\n", result.MaxConnect) + fmt.Fprintf(buf, "StopOthers: %v\n", result.StopOthers) + + // Build table of suggested connections. + buf.WriteString("\nSuggested Connections:\n") + tabWriter := tabwriter.NewWriter(buf, 8, 4, 3, ' ', 0) + fmt.Fprint(tabWriter, "Hub Name\tReason\tDuplicate\tCountry\tRegion\tLatency\tCapacity\tCost\tGeo Prox.\tHub ID\tLifetime Usage\tPeriod Usage\tProt\tStatus\n") + for _, suggested := range result.SuggestedConnections { + var dupe string + if suggested.Duplicate { + dupe = "yes" + } else { + // Only lock dupes once. + suggested.pin.measurements.Lock() + defer suggested.pin.measurements.Unlock() + } + + // Add row. + fmt.Fprintf(tabWriter, + "%s\t%s\t%s\t%s\t%s\t%s\t%.2fMbit/s\t%.2fc\t%.2f%%\t%s", + suggested.Hub.Info.Name, + suggested.Reason, + dupe, + getPinCountry(suggested.pin), + suggested.pin.region.getName(), + suggested.pin.measurements.Latency, + float64(suggested.pin.measurements.Capacity)/1000000, + suggested.pin.measurements.CalculatedCost, + suggested.pin.measurements.GeoProximity, + suggested.Hub.ID, + ) + + // Add usage stats. + if crane, ok := assignedCranes[suggested.Hub.ID]; ok { + addUsageStatsToTable(crane, tabWriter) + } + + // Add linebreak. + fmt.Fprint(tabWriter, "\n") + } + _ = tabWriter.Flush() + + return buf.Bytes(), nil +} + +// addUsageStatsToTable compiles some usage stats of a lane and addes them to the table. +// Table Fields: Lifetime Usage, Period Usage, Prot, Mine. +func addUsageStatsToTable(crane *docks.Crane, tabWriter *tabwriter.Writer) { + ltIn, ltOut, ltStart, pIn, pOut, pStart := crane.NetState.GetTrafficStats() + ltDuration := time.Since(ltStart) + pDuration := time.Since(pStart) + + // Build ownership and stopping info. + var status string + isMine := crane.IsMine() + isStopping := crane.IsStopping() + stoppingRequested, stoppingRequestedByPeer, markedStoppingAt := crane.NetState.StoppingState() + if isMine { + status = "mine" + } + if isStopping || stoppingRequested || stoppingRequestedByPeer { + if isMine { + status += " - " + } + status += "stopping " + if stoppingRequested { + status += " + +
+ + + + +`, + "`", graph.String(), "`", + )) + } + + // Write response. + w.Header().Set("Content-Type", mimeType+"; charset=utf-8") + w.Header().Set("Content-Length", strconv.Itoa(len(responseData))) + w.WriteHeader(http.StatusOK) + _, err := w.Write(responseData) + if err != nil { + log.Tracer(r.Context()).Warningf("api: failed to write response: %s", err) + } +} + +func graphNodeLabel(pin *Pin) (s string) { + var comment string + switch { + case pin.State == StateNone: + comment = "dead" + case pin.State.Has(StateIsHomeHub): + comment = "Home" + case pin.State.HasAnyOf(StateSummaryDisregard): + comment = "disregarded" + case !pin.State.Has(StateSummaryRegard): + comment = "not regarded" + case pin.State.Has(StateTrusted): + comment = "trusted" + } + if comment != "" { + comment = fmt.Sprintf("\n(%s)", comment) + } + + if pin.Hub.Status.Load >= 80 { + comment += fmt.Sprintf("\nHIGH LOAD: %d", pin.Hub.Status.Load) + } + + return fmt.Sprintf( + `"%s%s"`, + strings.ReplaceAll(pin.Hub.Name(), " ", "\n"), + comment, + ) +} + +func graphNodeTooltip(pin *Pin) string { + // Gather IP info. + var v4Info, v6Info string + if pin.Hub.Info.IPv4 != nil { + if pin.LocationV4 != nil { + v4Info = fmt.Sprintf( + "%s (%s AS%d %s)", + pin.Hub.Info.IPv4.String(), + pin.LocationV4.Country.Code, + pin.LocationV4.AutonomousSystemNumber, + pin.LocationV4.AutonomousSystemOrganization, + ) + } else { + v4Info = pin.Hub.Info.IPv4.String() + } + } + if pin.Hub.Info.IPv6 != nil { + if pin.LocationV6 != nil { + v6Info = fmt.Sprintf( + "%s (%s AS%d %s)", + pin.Hub.Info.IPv6.String(), + pin.LocationV6.Country.Code, + pin.LocationV6.AutonomousSystemNumber, + pin.LocationV6.AutonomousSystemOrganization, + ) + } else { + v6Info = pin.Hub.Info.IPv6.String() + } + } + + return fmt.Sprintf( + `"ID: %s +States: %s +Version: %s +IPv4: %s +IPv6: %s +Load: %d +Cost: %.2f"`, + pin.Hub.ID, + pin.State, + pin.Hub.Status.Version, + v4Info, + v6Info, + pin.Hub.Status.Load, + pin.Cost, + ) +} + +func graphEdgeTooltip(from, to *Pin, lane *Lane) string { + return fmt.Sprintf( + `"%s <> %s +Latency: %s +Capacity: %.2f Mbit/s +Cost: %.2f"`, + from.Hub.Info.Name, to.Hub.Info.Name, + lane.Latency, + float64(lane.Capacity)/1000000, + lane.Cost, + ) +} + +// Graphviz colors. +// See https://graphviz.org/doc/info/colors.html +const ( + graphColorWarning = "orange2" + graphColorError = "red2" + graphColorHomeAndConnected = "steelblue2" + graphColorDisregard = "tomato2" + graphColorNotRegard = "tan2" + graphColorTrusted = "seagreen2" + graphColorDefaultNode = "seashell2" + graphColorDefaultEdge = "black" + graphColorNone = "transparent" +) + +func graphNodeColor(pin *Pin) string { + switch { + case pin.State == StateNone: + return graphColorNone + case pin.Hub.Status.Load >= 95: + return graphColorError + case pin.Hub.Status.Load >= 80: + return graphColorWarning + case pin.State.Has(StateIsHomeHub): + return graphColorHomeAndConnected + case pin.State.HasAnyOf(StateSummaryDisregard): + return graphColorDisregard + case !pin.State.Has(StateSummaryRegard): + return graphColorNotRegard + case pin.State.Has(StateTrusted): + return graphColorTrusted + default: + return graphColorDefaultNode + } +} + +func graphNodeBorderColor(pin *Pin) string { + switch { + case pin.HasActiveTerminal(): + return graphColorHomeAndConnected + default: + return graphColorNone + } +} + +func graphEdgeColor(from, to *Pin, lane *Lane) string { + // Check lane stats. + if lane.Capacity == 0 || lane.Latency == 0 { + return graphColorWarning + } + // Alert if capacity is under 10Mbit/s or latency is over 100ms. + if lane.Capacity < 10000000 || lane.Latency > 100*time.Millisecond { + return graphColorError + } + + // Check for active edge forward. + if to.HasActiveTerminal() && len(to.Connection.Route.Path) >= 2 { + secondLastHopIndex := len(to.Connection.Route.Path) - 2 + if to.Connection.Route.Path[secondLastHopIndex].HubID == from.Hub.ID { + return graphColorHomeAndConnected + } + } + // Check for active edge backward. + if from.HasActiveTerminal() && len(from.Connection.Route.Path) >= 2 { + secondLastHopIndex := len(from.Connection.Route.Path) - 2 + if from.Connection.Route.Path[secondLastHopIndex].HubID == to.Hub.ID { + return graphColorHomeAndConnected + } + } + + // Return default color if edge is not active. + return graphColorDefaultEdge +} diff --git a/spn/navigator/api_route.go b/spn/navigator/api_route.go new file mode 100644 index 00000000..4d854841 --- /dev/null +++ b/spn/navigator/api_route.go @@ -0,0 +1,396 @@ +package navigator + +import ( + "bytes" + "errors" + "fmt" + mrand "math/rand" + "net" + "net/http" + "strings" + "text/tabwriter" + "time" + + "github.com/safing/portbase/api" + "github.com/safing/portbase/config" + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/service/intel/geoip" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/network/netutils" + "github.com/safing/portmaster/service/profile" + "github.com/safing/portmaster/service/profile/endpoints" +) + +func registerRouteAPIEndpoints() error { + if err := api.RegisterEndpoint(api.Endpoint{ + Path: `spn/map/{map:[A-Za-z0-9]{1,255}}/route/to/{destination:[a-z0-9_\.:-]{1,255}}`, + Read: api.PermitUser, + BelongsTo: module, + ActionFunc: handleRouteCalculationRequest, + Name: "Calculate Route through SPN", + Description: "Returns a textual representation of the routing process.", + Parameters: []api.Parameter{ + { + Method: http.MethodGet, + Field: "profile", + Value: "|global", + Description: "Specify a profile ID to load more settings for simulation.", + }, + { + Method: http.MethodGet, + Field: "encrypted", + Value: "true", + Description: "Specify to signify that the simulated connection should be regarded as encrypted. Only valid with a profile.", + }, + }, + }); err != nil { + return err + } + + return nil +} + +func handleRouteCalculationRequest(ar *api.Request) (msg string, err error) { //nolint:maintidx + // Get map. + m, ok := getMapForAPI(ar.URLVars["map"]) + if !ok { + return "", errors.New("map not found") + } + // Get profile ID. + profileID := ar.Request.URL.Query().Get("profile") + + // Parse destination and prepare options. + entity := &intel.Entity{} + destination := ar.URLVars["destination"] + matchFor := DestinationHub + var ( + introText string + locationV4, locationV6 *geoip.Location + opts *Options + ) + switch { + case destination == "": + // Destination is required. + return "", errors.New("no destination provided") + + case destination == "home": + if profileID != "" { + return "", errors.New("cannot apply profile to home hub route") + } + // Simulate finding home hub. + locations, ok := netenv.GetInternetLocation() + if !ok || len(locations.All) == 0 { + return "", errors.New("failed to locate own device for finding home hub") + } + introText = fmt.Sprintf("looking for home hub near %s and %s", locations.BestV4(), locations.BestV6()) + locationV4 = locations.BestV4().LocationOrNil() + locationV6 = locations.BestV6().LocationOrNil() + matchFor = HomeHub + + // START of copied from captain/navigation.go + + // Get own entity. + // Checking the entity against the entry policies is somewhat hit and miss + // anyway, as the device location is an approximation. + var myEntity *intel.Entity + if dl := locations.BestV4(); dl != nil && dl.IP != nil { + myEntity = (&intel.Entity{IP: dl.IP}).Init(0) + myEntity.FetchData(ar.Context()) + } else if dl := locations.BestV6(); dl != nil && dl.IP != nil { + myEntity = (&intel.Entity{IP: dl.IP}).Init(0) + myEntity.FetchData(ar.Context()) + } + + // Build navigation options for searching for a home hub. + homePolicy, err := endpoints.ParseEndpoints(config.GetAsStringArray("spn/homePolicy", []string{})()) + if err != nil { + return "", fmt.Errorf("failed to parse home hub policy: %w", err) + } + + opts = &Options{ + Home: &HomeHubOptions{ + HubPolicies: []endpoints.Endpoints{homePolicy}, + CheckHubPolicyWith: myEntity, + }, + } + + // Add requirement to only use Safing nodes when not using community nodes. + if !config.GetAsBool("spn/useCommunityNodes", true)() { + opts.Home.RequireVerifiedOwners = []string{"Safing"} + } + + // Require a trusted home node when the routing profile requires less than two hops. + routingProfile := GetRoutingProfile(config.GetAsString(profile.CfgOptionRoutingAlgorithmKey, DefaultRoutingProfileID)()) + if routingProfile.MinHops < 2 { + opts.Home.Regard = opts.Home.Regard.Add(StateTrusted) + } + + // END of copied + + case net.ParseIP(destination) != nil: + entity.IP = net.ParseIP(destination) + + fallthrough + case netutils.IsValidFqdn(destination): + fallthrough + case netutils.IsValidFqdn(destination + "."): + // Resolve domain to IP, if not inherired from a previous case. + var ignoredIPs int + if entity.IP == nil { + entity.Domain = destination + + // Resolve name to IPs. + ips, err := net.DefaultResolver.LookupIP(ar.Context(), "ip", destination) + if err != nil { + return "", fmt.Errorf("failed to lookup IP address of %s: %w", destination, err) + } + if len(ips) == 0 { + return "", fmt.Errorf("failed to lookup IP address of %s: no result", destination) + } + + // Shuffle IPs. + if len(ips) >= 2 { + mr := mrand.New(mrand.NewSource(time.Now().UnixNano())) //nolint:gosec + mr.Shuffle(len(ips), func(i, j int) { + ips[i], ips[j] = ips[j], ips[i] + }) + } + + entity.IP = ips[0] + ignoredIPs = len(ips) - 1 + } + entity.Init(0) + + // Get location of IP. + location, ok := entity.GetLocation(ar.Context()) + if !ok { + return "", fmt.Errorf("failed to get geoip location for %s: %s", entity.IP, entity.LocationError) + } + // Assign location to separate variables. + if entity.IP.To4() != nil { + locationV4 = location + } else { + locationV6 = location + } + + // Set intro text. + if entity.Domain != "" { + introText = fmt.Sprintf("looking for route to %s at %s\n(ignoring %d additional IPs returned by DNS)", entity.IP, formatLocation(location), ignoredIPs) + } else { + introText = fmt.Sprintf("looking for route to %s at %s", entity.IP, formatLocation(location)) + } + + // Get profile. + if profileID != "" { + var lp *profile.LayeredProfile + if profileID == "global" { + // Create new empty profile for easy access to global settings. + lp = profile.NewLayeredProfile(profile.New(nil)) + } else { + // Get local profile by ID. + localProfile, err := profile.GetLocalProfile(profileID, nil, nil) + if err != nil { + return "", fmt.Errorf("failed to get profile: %w", err) + } + lp = localProfile.LayeredProfile() + } + opts = DeriveTunnelOptions( + lp, + entity, + ar.Request.URL.Query().Has("encrypted"), + ) + } else { + opts = m.defaultOptions() + } + + default: + return "", errors.New("invalid destination provided") + } + + // Finalize entity. + entity.Init(0) + + // Start formatting output. + lines := []string{ + "Routing simulation: " + introText, + "Please note that this routing simulation does match the behavior of regular routing to 100%.", + "", + } + + // Print options. + // ================== + + lines = append(lines, "Routing Options:") + lines = append(lines, "Algorithm: "+opts.RoutingProfile) + if opts.Home != nil { + lines = append(lines, "Home Options:") + lines = append(lines, fmt.Sprintf(" Regard: %s", opts.Home.Regard)) + lines = append(lines, fmt.Sprintf(" Disregard: %s", opts.Home.Disregard)) + lines = append(lines, fmt.Sprintf(" No Default: %v", opts.Home.NoDefaults)) + lines = append(lines, fmt.Sprintf(" Hub Policies: %v", opts.Home.HubPolicies)) + lines = append(lines, fmt.Sprintf(" Require Verified Owners: %v", opts.Home.RequireVerifiedOwners)) + } + if opts.Transit != nil { + lines = append(lines, "Transit Options:") + lines = append(lines, fmt.Sprintf(" Regard: %s", opts.Transit.Regard)) + lines = append(lines, fmt.Sprintf(" Disregard: %s", opts.Transit.Disregard)) + lines = append(lines, fmt.Sprintf(" No Default: %v", opts.Transit.NoDefaults)) + lines = append(lines, fmt.Sprintf(" Hub Policies: %v", opts.Transit.HubPolicies)) + lines = append(lines, fmt.Sprintf(" Require Verified Owners: %v", opts.Transit.RequireVerifiedOwners)) + } + if opts.Destination != nil { + lines = append(lines, "Destination Options:") + lines = append(lines, fmt.Sprintf(" Regard: %s", opts.Destination.Regard)) + lines = append(lines, fmt.Sprintf(" Disregard: %s", opts.Destination.Disregard)) + lines = append(lines, fmt.Sprintf(" No Default: %v", opts.Destination.NoDefaults)) + lines = append(lines, fmt.Sprintf(" Hub Policies: %v", opts.Destination.HubPolicies)) + lines = append(lines, fmt.Sprintf(" Require Verified Owners: %v", opts.Destination.RequireVerifiedOwners)) + if opts.Destination.CheckHubPolicyWith != nil { + lines = append(lines, " Check Hub Policy With:") + if opts.Destination.CheckHubPolicyWith.Domain != "" { + lines = append(lines, fmt.Sprintf(" Domain: %v", opts.Destination.CheckHubPolicyWith.Domain)) + } + if opts.Destination.CheckHubPolicyWith.IP != nil { + lines = append(lines, fmt.Sprintf(" IP: %v", opts.Destination.CheckHubPolicyWith.IP)) + } + if opts.Destination.CheckHubPolicyWith.Port != 0 { + lines = append(lines, fmt.Sprintf(" Port: %v", opts.Destination.CheckHubPolicyWith.Port)) + } + } + } + lines = append(lines, "\n") + + // Find nearest hubs. + // ================== + + // Start operating in map. + m.RLock() + defer m.RUnlock() + // Check if map is populated. + if m.isEmpty() { + return "", ErrEmptyMap + } + + // Find nearest hubs. + nbPins, err := m.findNearestPins(locationV4, locationV6, opts, matchFor, true) + if err != nil { + lines = append(lines, fmt.Sprintf("FAILED to find any suitable exit hub: %s", err)) + return strings.Join(lines, "\n"), nil + // return "", fmt.Errorf("failed to search for nearby pins: %w", err) + } + + // Print found exits to table. + lines = append(lines, "Considered Exits (cheapest 10% are shuffled)") + buf := bytes.NewBuffer(nil) + tabWriter := tabwriter.NewWriter(buf, 8, 4, 3, ' ', 0) + fmt.Fprint(tabWriter, "Hub Name\tCost\tLocation\n") + for _, nbPin := range nbPins.pins { + fmt.Fprintf(tabWriter, + "%s\t%.0f\t%s\n", + nbPin.pin.Hub.Name(), + nbPin.cost, + formatMultiLocation(nbPin.pin.LocationV4, nbPin.pin.LocationV6), + ) + } + _ = tabWriter.Flush() + lines = append(lines, buf.String()) + + // Print too expensive exits to table. + lines = append(lines, "Too Expensive Exits:") + buf = bytes.NewBuffer(nil) + tabWriter = tabwriter.NewWriter(buf, 8, 4, 3, ' ', 0) + fmt.Fprint(tabWriter, "Hub Name\tCost\tLocation\n") + for _, nbPin := range nbPins.debug.tooExpensive { + fmt.Fprintf(tabWriter, + "%s\t%.0f\t%s\n", + nbPin.pin.Hub.Name(), + nbPin.cost, + formatMultiLocation(nbPin.pin.LocationV4, nbPin.pin.LocationV6), + ) + } + _ = tabWriter.Flush() + lines = append(lines, buf.String()) + + // Print disregarded exits to table. + lines = append(lines, "Disregarded Exits:") + buf = bytes.NewBuffer(nil) + tabWriter = tabwriter.NewWriter(buf, 8, 4, 3, ' ', 0) + fmt.Fprint(tabWriter, "Hub Name\tReason\tStates\n") + for _, nbPin := range nbPins.debug.disregarded { + fmt.Fprintf(tabWriter, + "%s\t%s\t%s\n", + nbPin.pin.Hub.Name(), + nbPin.reason, + nbPin.pin.State, + ) + } + _ = tabWriter.Flush() + lines = append(lines, buf.String()) + + // Find routes. + // ============ + + // Unless we looked for a home node. + if destination == "home" { + return strings.Join(lines, "\n"), nil + } + + // Find routes. + routes, err := m.findRoutes(nbPins, opts) + if err != nil { + lines = append(lines, fmt.Sprintf("FAILED to find routes: %s", err)) + return strings.Join(lines, "\n"), nil + // return "", fmt.Errorf("failed to find routes: %w", err) + } + + // Print found routes to table. + lines = append(lines, "Considered Routes (cheapest 10% are shuffled)") + buf = bytes.NewBuffer(nil) + tabWriter = tabwriter.NewWriter(buf, 8, 4, 3, ' ', 0) + fmt.Fprint(tabWriter, "Cost\tPath\n") + for _, route := range routes.All { + fmt.Fprintf(tabWriter, + "%.0f\t%s\n", + route.TotalCost, + formatRoute(route, entity.IP), + ) + } + _ = tabWriter.Flush() + lines = append(lines, buf.String()) + + return strings.Join(lines, "\n"), nil +} + +func formatLocation(loc *geoip.Location) string { + return fmt.Sprintf( + "%s (%s - AS%d %s)", + loc.Country.Name, + loc.Country.Code, + loc.AutonomousSystemNumber, + loc.AutonomousSystemOrganization, + ) +} + +func formatMultiLocation(a, b *geoip.Location) string { + switch { + case a != nil: + return formatLocation(a) + case b != nil: + return formatLocation(b) + default: + return "" + } +} + +func formatRoute(r *Route, dst net.IP) string { + s := make([]string, 0, len(r.Path)+1) + for i, hop := range r.Path { + if i == 0 { + s = append(s, hop.pin.Hub.Name()) + } else { + s = append(s, fmt.Sprintf(">> %.2fc >> %s", hop.Cost, hop.pin.Hub.Name())) + } + } + s = append(s, fmt.Sprintf(">> %.2fc >> %s", r.DstCost, dst)) + return strings.Join(s, " ") +} diff --git a/spn/navigator/costs.go b/spn/navigator/costs.go new file mode 100644 index 00000000..0b48ea16 --- /dev/null +++ b/spn/navigator/costs.go @@ -0,0 +1,72 @@ +package navigator + +import "time" + +const ( + nearestPinsMaxCostDifference = 5000 + nearestPinsMinimum = 10 +) + +// CalculateLaneCost calculates the cost of using a Lane based on the given +// Lane latency and capacity. +// Ranges from 0 to 10000. +func CalculateLaneCost(latency time.Duration, capacity int) (cost float32) { + // - One point for every ms in latency (linear) + if latency != 0 { + cost += float32(latency) / float32(time.Millisecond) + } else { + // Add cautious default cost if latency is not available. + cost += 1000 + } + + capacityFloat := float32(capacity) + switch { + case capacityFloat == 0: + // Add cautious default cost if capacity is not available. + cost += 4000 + case capacityFloat < cap1Mbit: + // - Between 1000 and 10000 points for ranges below 1Mbit/s + cost += 1000 + 9000*((cap1Mbit-capacityFloat)/cap1Mbit) + case capacityFloat < cap10Mbit: + // - Between 100 and 1000 points for ranges below 10Mbit/s + cost += 100 + 900*((cap10Mbit-capacityFloat)/cap10Mbit) + case capacityFloat < cap100Mbit: + // - Between 20 and 100 points for ranges below 100Mbit/s + cost += 20 + 80*((cap100Mbit-capacityFloat)/cap100Mbit) + case capacityFloat < cap1Gbit: + // - Between 5 and 20 points for ranges below 1Gbit/s + cost += 5 + 15*((cap1Gbit-capacityFloat)/cap1Gbit) + case capacityFloat < cap10Gbit: + // - Between 0 and 5 points for ranges below 10Gbit/s + cost += 5 * ((cap10Gbit - float32(capacity)) / cap10Gbit) + } + + return cost +} + +// CalculateHubCost calculates the cost of using a Hub based on the given Hub load. +// Ranges from 100 to 10000. +func CalculateHubCost(load int) (cost float32) { + switch { + case load >= 100: + return 10000 + case load >= 95: + return 1000 + case load >= 80: + return 500 + default: + return 100 + } +} + +// CalculateDestinationCost calculates the cost of a destination hub to a +// destination server based on the given proximity. +// Ranges from 0 to 2500. +func CalculateDestinationCost(proximity float32) (cost float32) { + // Invert from proximity (0-100) to get a distance value. + distance := 100 - proximity + + // Take the distance to the power of three and then divide by hundred in order to + // make high distances exponentially more expensive. + return (distance * distance * distance) / 100 +} diff --git a/spn/navigator/database.go b/spn/navigator/database.go new file mode 100644 index 00000000..b7ee8ae4 --- /dev/null +++ b/spn/navigator/database.go @@ -0,0 +1,164 @@ +package navigator + +import ( + "context" + "fmt" + "strings" + + "github.com/safing/portbase/database" + "github.com/safing/portbase/database/iterator" + "github.com/safing/portbase/database/query" + "github.com/safing/portbase/database/record" + "github.com/safing/portbase/database/storage" +) + +var mapDBController *database.Controller + +// StorageInterface provices a storage.Interface to the +// configuration manager. +type StorageInterface struct { + storage.InjectBase +} + +// Database prefixes: +// Pins: map:main/ +// DNS Requests: network:tree//dns/ +// IP Connections: network:tree//ip/ + +func makeDBKey(mapName, hubID string) string { + return fmt.Sprintf("map:%s/%s", mapName, hubID) +} + +func parseDBKey(key string) (mapName, hubID string) { + // Split into segments. + segments := strings.Split(key, "/") + + // Keys have 1 or 2 segments. + switch len(segments) { + case 1: + return segments[0], "" + case 2: + return segments[0], segments[1] + default: + return "", "" + } +} + +// Get returns a database record. +func (s *StorageInterface) Get(key string) (record.Record, error) { + // Parse key and check if valid. + mapName, hubID := parseDBKey(key) + if mapName == "" || hubID == "" { + return nil, storage.ErrNotFound + } + + // Get map. + m, ok := getMapForAPI(mapName) + if !ok { + return nil, storage.ErrNotFound + } + + // Get Pin from map. + pin, ok := m.GetPin(hubID) + if !ok { + return nil, storage.ErrNotFound + } + return pin.Export(), nil +} + +// Query returns a an iterator for the supplied query. +func (s *StorageInterface) Query(q *query.Query, local, internal bool) (*iterator.Iterator, error) { + // Parse key and check if valid. + mapName, _ := parseDBKey(q.DatabaseKeyPrefix()) + if mapName == "" { + return nil, storage.ErrNotFound + } + + // Get map. + m, ok := getMapForAPI(mapName) + if !ok { + return nil, storage.ErrNotFound + } + + // Start query worker. + it := iterator.New() + module.StartWorker("map query", func(_ context.Context) error { + s.processQuery(m, q, it) + return nil + }) + + return it, nil +} + +func (s *StorageInterface) processQuery(m *Map, q *query.Query, it *iterator.Iterator) { + // Return all matching pins. + for _, pin := range m.sortedPins(true) { + export := pin.Export() + if q.Matches(export) { + select { + case it.Next <- export: + case <-it.Done: + return + } + } + } + + it.Finish(nil) +} + +func registerMapDatabase() error { + _, err := database.Register(&database.Database{ + Name: "map", + Description: "SPN Network Maps", + StorageType: database.StorageTypeInjected, + }) + if err != nil { + return err + } + + controller, err := database.InjectDatabase("map", &StorageInterface{}) + if err != nil { + return err + } + + mapDBController = controller + return nil +} + +func withdrawMapDatabase() { + mapDBController.Withdraw() +} + +// PushPinChanges pushes all changed pins to subscribers. +func (m *Map) PushPinChanges() { + module.StartWorker("push pin changes", m.pushPinChangesWorker) +} + +func (m *Map) pushPinChangesWorker(ctx context.Context) error { + m.RLock() + defer m.RUnlock() + + for _, pin := range m.all { + if pin.pushChanges.SetToIf(true, false) { + mapDBController.PushUpdate(pin.Export()) + } + } + + return nil +} + +// pushChange pushes changes of the pin, if the pushChanges flag is set. +func (pin *Pin) pushChange() { + // Check before starting the worker. + if pin.pushChanges.IsNotSet() { + return + } + + // Start worker to push changes. + module.StartWorker("push pin change", func(ctx context.Context) error { + if pin.pushChanges.SetToIf(true, false) { + mapDBController.PushUpdate(pin.Export()) + } + return nil + }) +} diff --git a/spn/navigator/findnearest.go b/spn/navigator/findnearest.go new file mode 100644 index 00000000..0a294ce2 --- /dev/null +++ b/spn/navigator/findnearest.go @@ -0,0 +1,441 @@ +package navigator + +import ( + "errors" + "fmt" + mrand "math/rand" + "sort" + "strings" + "time" + + "github.com/safing/portmaster/service/intel/geoip" + "github.com/safing/portmaster/spn/hub" +) + +const ( + // defaultMaxNearbyMatches defines a default value of how many matches a + // nearby pin find operation in a map should return. + defaultMaxNearbyMatches = 100 + + // defaultRandomizeNearbyPinTopPercent defines the top percent of a nearby + // pins set that should be randomized for balancing purposes. + // Range: 0-1. + defaultRandomizeNearbyPinTopPercent = 0.1 +) + +// nearbyPins is a list of nearby Pins to a certain location. +type nearbyPins struct { + pins []*nearbyPin + minPins int + maxPins int + maxCost float32 + cutOffLimit float32 + randomizeTopPercent float32 + + debug *nearbyPinsDebug +} + +// nearbyPinsDebug holds additional debugging for nearbyPins. +type nearbyPinsDebug struct { + tooExpensive []*nearbyPin + disregarded []*nearbyDisregardedPin +} + +// nearbyDisregardedPin represents a disregarded pin. +type nearbyDisregardedPin struct { + pin *Pin + reason string +} + +// nearbyPin represents a Pin and the proximity to a certain location. +type nearbyPin struct { + pin *Pin + cost float32 +} + +// Len is the number of elements in the collection. +func (nb *nearbyPins) Len() int { + return len(nb.pins) +} + +// Less reports whether the element with index i should sort before the element +// with index j. +func (nb *nearbyPins) Less(i, j int) bool { + return nb.pins[i].cost < nb.pins[j].cost +} + +// Swap swaps the elements with indexes i and j. +func (nb *nearbyPins) Swap(i, j int) { + nb.pins[i], nb.pins[j] = nb.pins[j], nb.pins[i] +} + +// add potentially adds a Pin to the list of nearby Pins. +func (nb *nearbyPins) add(pin *Pin, cost float32) { + if len(nb.pins) > nb.minPins && nb.maxCost > 0 && cost > nb.maxCost { + // Add debug data if enabled. + if nb.debug != nil { + nb.debug.tooExpensive = append(nb.debug.tooExpensive, + &nearbyPin{ + pin: pin, + cost: cost, + }, + ) + } + + return + } + + nb.pins = append(nb.pins, &nearbyPin{ + pin: pin, + cost: cost, + }) +} + +// contains checks if the collection contains a Pin. +func (nb *nearbyPins) get(id string) *nearbyPin { + for _, nbPin := range nb.pins { + if nbPin.pin.Hub.ID == id { + return nbPin + } + } + + return nil +} + +// clean sort and shortens the list to the configured maximum. +func (nb *nearbyPins) clean() { + // Sort nearby Pins so that the closest one is on top. + sort.Sort(nb) + + // Set maximum cost based on max difference, if we have enough pins. + if len(nb.pins) >= nb.minPins { + nb.maxCost = nb.pins[0].cost + nb.cutOffLimit + } + + // Remove superfluous Pins from the list. + if len(nb.pins) > nb.maxPins { + // Add debug data if enabled. + if nb.debug != nil { + nb.debug.tooExpensive = append(nb.debug.tooExpensive, nb.pins[nb.maxPins:]...) + } + + nb.pins = nb.pins[:nb.maxPins] + } + // Remove Pins that are too costly. + if len(nb.pins) > nb.minPins { + // Search for first pin that is too costly. + okUntil := nb.minPins + for ; okUntil < len(nb.pins); okUntil++ { + if nb.pins[okUntil].cost > nb.maxCost { + break + } + } + + // Add debug data if enabled. + if nb.debug != nil { + nb.debug.tooExpensive = append(nb.debug.tooExpensive, nb.pins[okUntil:]...) + } + + // Cut off the list at that point. + nb.pins = nb.pins[:okUntil] + } +} + +// randomizeTop randomized to the top nearest pins for balancing the network. +func (nb *nearbyPins) randomizeTop() { + switch { + case nb.randomizeTopPercent == 0: + // Check if randomization is enabled. + return + case len(nb.pins) < 2: + // Check if we have enough pins to work with. + return + } + + // Find randomization set. + randomizeUpTo := len(nb.pins) + threshold := nb.pins[0].cost * (1 + nb.randomizeTopPercent) + for i, nb := range nb.pins { + // Find first value above the threshold to stop. + if nb.cost > threshold { + randomizeUpTo = i + break + } + } + + // Shuffle top set. + if randomizeUpTo >= 2 { + mr := mrand.New(mrand.NewSource(time.Now().UnixNano())) //nolint:gosec + mr.Shuffle(randomizeUpTo, nb.Swap) + } +} + +// FindNearestHubs searches for the nearest Hubs to the given IP address. The returned Hubs must not be modified in any way. +func (m *Map) FindNearestHubs(locationV4, locationV6 *geoip.Location, opts *Options, matchFor HubType) ([]*hub.Hub, error) { + m.RLock() + defer m.RUnlock() + + // Check if map is populated. + if m.isEmpty() { + return nil, ErrEmptyMap + } + + // Set default options if unset. + if opts == nil { + opts = m.defaultOptions() + } + + // Find nearest Pins. + nearby, err := m.findNearestPins(locationV4, locationV6, opts, matchFor, false) + if err != nil { + return nil, err + } + + // Convert to Hub list and return. + hubs := make([]*hub.Hub, 0, len(nearby.pins)) + for _, nbPin := range nearby.pins { + hubs = append(hubs, nbPin.pin.Hub) + } + return hubs, nil +} + +func (m *Map) findNearestPins(locationV4, locationV6 *geoip.Location, opts *Options, matchFor HubType, debug bool) (*nearbyPins, error) { + // Fail if no location is provided. + if locationV4 == nil && locationV6 == nil { + return nil, errors.New("no location provided") + } + + // Raise maxMatches to nearestPinsMinimum. + maxMatches := defaultMaxNearbyMatches + if maxMatches < nearestPinsMinimum { + maxMatches = nearestPinsMinimum + } + + // Create nearby Pins list. + nearby := &nearbyPins{ + minPins: nearestPinsMinimum, + maxPins: maxMatches, + cutOffLimit: nearestPinsMaxCostDifference, + randomizeTopPercent: defaultRandomizeNearbyPinTopPercent, + } + if debug { + nearby.debug = &nearbyPinsDebug{} + } + + // Create pin matcher. + matcher := opts.Matcher(matchFor, m.intel) + + // Iterate over all Pins in the Map to find the nearest ones. + for _, pin := range m.all { + var cost float32 + + // Check if the Pin matches the criteria. + if !matcher(pin) { + // Add debug data if enabled. + if nearby.debug != nil && pin.State.Has(StateActive|StateReachable) { + nearby.debug.disregarded = append(nearby.debug.disregarded, + &nearbyDisregardedPin{ + pin: pin, + reason: "does not match general criteria", + }, + ) + } + + // Debugging: + // log.Tracef("spn/navigator: skipping %s with states %s for finding nearest", pin, pin.State) + continue + } + + // Check if the Hub supports at least one IP version we are looking for. + switch { + case locationV4 != nil && pin.LocationV4 != nil: + // Both have IPv4! + case locationV6 != nil && pin.LocationV6 != nil: + // Both have IPv6! + default: + // Hub does not support any IP version we need. + + // Add debug data if enabled. + if nearby.debug != nil { + nearby.debug.disregarded = append(nearby.debug.disregarded, + &nearbyDisregardedPin{ + pin: pin, + reason: "does not support the required IP version", + }, + ) + } + + continue + } + + // If finding a home hub and the global routing profile is set to home ("VPN"), + // check if all local IP versions are available on the Hub. + if matchFor == HomeHub && cfgOptionRoutingAlgorithm() == RoutingProfileHomeID { + switch { + case locationV4 != nil && pin.LocationV4 == nil: + // Device has IPv4, but Hub does not! + fallthrough + case locationV6 != nil && pin.LocationV6 == nil: + // Device has IPv6, but Hub does not! + + // Add debug data if enabled. + if nearby.debug != nil { + nearby.debug.disregarded = append(nearby.debug.disregarded, + &nearbyDisregardedPin{ + pin: pin, + reason: "home hub needs all IP versions of client (when Home/VPN routing)", + }, + ) + } + + continue + } + } + + // 1. Calculate cost based on distance + + if locationV4 != nil && pin.LocationV4 != nil { + if locationV4.IsAnycast && m.home != nil { + // If the destination is anycast, calculate cost though proximity to home hub instead, if possible. + cost = lessButPositive(cost, CalculateDestinationCost( + proximityBetweenPins(pin, m.home), + )) + } else { + // Regular cost calculation through proximity. + cost = lessButPositive(cost, CalculateDestinationCost( + locationV4.EstimateNetworkProximity(pin.LocationV4), + )) + } + } + + if locationV6 != nil && pin.LocationV6 != nil { + if locationV6.IsAnycast && m.home != nil { + // If the destination is anycast, calculate cost though proximity to home hub instead, if possible. + cost = lessButPositive(cost, CalculateDestinationCost( + proximityBetweenPins(pin, m.home), + )) + } else { + // Regular cost calculation through proximity. + cost = lessButPositive(cost, CalculateDestinationCost( + locationV6.EstimateNetworkProximity(pin.LocationV6), + )) + } + } + + // If no cost could be calculated, fall back to a default value. + if cost == 0 { + cost = CalculateDestinationCost(50) // proximity out of 0-100 + } + + // Debugging: + // if matchFor == HomeHub { + // log.Tracef("spn/navigator: adding %.2f proximity cost to home hub %s", cost, pin.Hub) + // } + + // 2. Add cost based on Hub status + + cost += CalculateHubCost(pin.Hub.Status.Load) + + // Debugging: + // if matchFor == HomeHub { + // log.Tracef("spn/navigator: adding %.2f hub cost to home hub %s", CalculateHubCost(pin.Hub.Status.Load), pin.Hub) + // } + + // 3. If matching a home hub, add cost based on capacity/latency performance. + + if matchFor == HomeHub { + // Find best capacity/latency values. + var ( + bestCapacity int + bestLatency time.Duration + ) + for _, lane := range pin.Hub.Status.Lanes { + if lane.Capacity > bestCapacity { + bestCapacity = lane.Capacity + } + if bestLatency == 0 || lane.Latency < bestLatency { + bestLatency = lane.Latency + } + } + // Add cost of best capacity/latency values. + cost += CalculateLaneCost(bestLatency, bestCapacity) + + // Debugging: + // log.Tracef("spn/navigator: adding %.2f lane cost to home hub %s", CalculateLaneCost(bestLatency, bestCapacity), pin.Hub) + // log.Debugf("spn/navigator: total cost of %.2f to home hub %s", cost, pin.Hub) + } + + nearby.add(pin, cost) + + // Clean the nearby list if have collected more than two times the max amount. + if len(nearby.pins) >= nearby.maxPins*2 { + nearby.clean() + } + } + + // Check if we found any nearby pins + if nearby.Len() == 0 { + return nil, ErrAllPinsDisregarded + } + + // Clean one last time and return the list. + nearby.clean() + + // Randomize top nearest pins for load balancing. + nearby.randomizeTop() + + // Debugging: + // if matchFor == HomeHub { + // log.Debug("spn/navigator: nearby pins:") + // for _, nbPin := range nearby.pins { + // log.Debugf("spn/navigator: nearby pin %s", nbPin) + // } + // } + + return nearby, nil +} + +func (nb *nearbyPins) String() string { + s := make([]string, 0, len(nb.pins)) + for _, nbPin := range nb.pins { + s = append(s, nbPin.String()) + } + return strings.Join(s, ", ") +} + +func (nb *nearbyPin) String() string { + return fmt.Sprintf("%s at %.2fc", nb.pin, nb.cost) +} + +func proximityBetweenPins(a, b *Pin) float32 { + var x, y float32 + + // Get IPv4 network proximity. + if a.LocationV4 != nil && b.LocationV4 != nil { + x = a.LocationV4.EstimateNetworkProximity(b.LocationV4) + } + + // Get IPv6 network proximity. + if a.LocationV6 != nil && b.LocationV6 != nil { + y = a.LocationV6.EstimateNetworkProximity(b.LocationV6) + } + + // Return higher proximity. + if x > y { + return x + } + return y +} + +func lessButPositive(a, b float32) float32 { + switch { + case a == 0: + return b + case b == 0: + return a + case a < b: + return a + default: + return b + } +} diff --git a/spn/navigator/findnearest_test.go b/spn/navigator/findnearest_test.go new file mode 100644 index 00000000..596d7779 --- /dev/null +++ b/spn/navigator/findnearest_test.go @@ -0,0 +1,124 @@ +package navigator + +import ( + "testing" +) + +func TestFindNearest(t *testing.T) { + t.Parallel() + + // Create map and lock faking in order to guarantee reproducability of faked data. + m := getDefaultTestMap() + fakeLock.Lock() + defer fakeLock.Unlock() + + for i := 0; i < 100; i++ { + // Create a random destination address + ip4, loc4 := createGoodIP(true) + + nbPins, err := m.findNearestPins(loc4, nil, m.DefaultOptions(), DestinationHub, false) + if err != nil { + t.Error(err) + } else { + t.Logf("Pins near %s: %s", ip4, nbPins) + } + } + + for i := 0; i < 100; i++ { + // Create a random destination address + ip6, loc6 := createGoodIP(true) + + nbPins, err := m.findNearestPins(nil, loc6, m.DefaultOptions(), DestinationHub, false) + if err != nil { + t.Error(err) + } else { + t.Logf("Pins near %s: %s", ip6, nbPins) + } + } +} + +/* +TODO: Find a way to quickly generate good geoip data on the fly, as we don't want to measure IP address generation, but only finding the nearest pins. + +func BenchmarkFindNearest(b *testing.B) { + // Create map and lock faking in order to guarantee reproducability of faked data. + m := getDefaultTestMap() + fakeLock.Lock() + defer fakeLock.Unlock() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + // Create a random destination address + var dstIP net.IP + if i%2 == 0 { + dstIP = net.ParseIP(gofakeit.IPv4Address()) + } else { + dstIP = net.ParseIP(gofakeit.IPv6Address()) + } + + _, err := m.findNearestPins(dstIP, m.DefaultOptions(),DestinationHub if err != nil { + b.Error(err) + } + } +} +*/ + +func findFakeHomeHub(m *Map) { + // Create fake IP address. + _, loc4 := createGoodIP(true) + _, loc6 := createGoodIP(false) + + nbPins, err := m.findNearestPins(loc4, loc6, m.defaultOptions(), HomeHub, false) + if err != nil { + panic(err) + } + if len(nbPins.pins) == 0 { + panic("could not find a Home Hub") + } + + // Set Home. + m.home = nbPins.pins[0].pin + + // Recalculate reachability. + if err := m.recalculateReachableHubs(); err != nil { + panic(err) + } +} + +func TestNearbyPinsCleaning(t *testing.T) { + t.Parallel() + + testCleaning(t, []float32{10, 20, 30, 40, 50, 60, 70, 80, 90, 100}, 3) + testCleaning(t, []float32{10, 11, 12, 13, 50, 60, 70, 80, 90, 100}, 4) + testCleaning(t, []float32{10, 11, 12, 40, 50, 60, 70, 80, 90, 100}, 3) + testCleaning(t, []float32{10, 11, 30, 40, 50, 60, 70, 80, 90, 100}, 3) +} + +func testCleaning(t *testing.T, costs []float32, expectedLeftOver int) { + t.Helper() + + nb := &nearbyPins{ + minPins: 3, + maxPins: 5, + cutOffLimit: 10, + } + + // Simulate usage. + for _, cost := range costs { + // Add to list. + nb.add(nil, cost) + + // Clean once in a while. + if len(nb.pins) > nb.maxPins { + nb.clean() + } + } + // Final clean. + nb.clean() + + // Check results. + t.Logf("result: %+v", nb.pins) + if len(nb.pins) != expectedLeftOver { + t.Errorf("unexpected amount of left over pins: %+v", nb.pins) + } +} diff --git a/spn/navigator/findroutes.go b/spn/navigator/findroutes.go new file mode 100644 index 00000000..ef886334 --- /dev/null +++ b/spn/navigator/findroutes.go @@ -0,0 +1,234 @@ +package navigator + +import ( + "errors" + "fmt" + "net" + + "github.com/safing/portmaster/service/intel/geoip" +) + +const ( + // defaultMaxRouteMatches defines a default value of how many matches a + // route find operation in a map should return. + defaultMaxRouteMatches = 10 + + // defaultRandomizeRoutesTopPercent defines the top percent of a routes + // set that should be randomized for balancing purposes. + // Range: 0-1. + defaultRandomizeRoutesTopPercent = 0.1 +) + +// FindRoutes finds possible routes to the given IP, with the given options. +func (m *Map) FindRoutes(ip net.IP, opts *Options) (*Routes, error) { + m.Lock() + defer m.Unlock() + + // Check if map is populated. + if m.isEmpty() { + return nil, ErrEmptyMap + } + + // Check if home hub is set. + if m.home == nil { + return nil, ErrHomeHubUnset + } + + // Get the location of the given IP address. + var locationV4, locationV6 *geoip.Location + var err error + // Save whether the given IP address is a IPv4 or IPv6 address. + if v4 := ip.To4(); v4 != nil { + locationV4, err = geoip.GetLocation(ip) + } else { + locationV6, err = geoip.GetLocation(ip) + } + if err != nil { + return nil, fmt.Errorf("failed to get IP location: %w", err) + } + + // Set default options if unset. + if opts == nil { + opts = m.defaultOptions() + } + + // Handle special home routing profile. + if opts.RoutingProfile == RoutingProfileHomeID { + switch { + case locationV4 != nil && m.home.LocationV4 == nil: + // Destination is IPv4, but Hub has no IPv4! + // Upgrade routing profile. + opts.RoutingProfile = RoutingProfileSingleHopID + + case locationV6 != nil && m.home.LocationV6 == nil: + // Destination is IPv6, but Hub has no IPv6! + // Upgrade routing profile. + opts.RoutingProfile = RoutingProfileSingleHopID + + default: + // Return route with only home hub for home hub routing. + return &Routes{ + All: []*Route{{ + Path: []*Hop{{ + pin: m.home, + HubID: m.home.Hub.ID, + }}, + Algorithm: RoutingProfileHomeID, + }}, + }, nil + } + } + + // Find nearest Pins. + nearby, err := m.findNearestPins(locationV4, locationV6, opts, DestinationHub, false) + if err != nil { + return nil, err + } + + return m.findRoutes(nearby, opts) +} + +// FindRouteToHub finds possible routes to the given Hub, with the given options. +func (m *Map) FindRouteToHub(hubID string, opts *Options) (*Routes, error) { + m.Lock() + defer m.Unlock() + + // Get Pin. + pin, ok := m.all[hubID] + if !ok { + return nil, ErrHubNotFound + } + + // Create a nearby with a single Pin. + nearby := &nearbyPins{ + pins: []*nearbyPin{ + { + pin: pin, + }, + }, + } + + // Find a route to the given Hub. + return m.findRoutes(nearby, opts) +} + +func (m *Map) findRoutes(dsts *nearbyPins, opts *Options) (*Routes, error) { + if m.home == nil { + return nil, ErrHomeHubUnset + } + + // Initialize matchers. + var done bool + transitMatcher := opts.Transit.Matcher(m.intel) + destinationMatcher := opts.Destination.Matcher(m.intel) + routingProfile := GetRoutingProfile(opts.RoutingProfile) + + // Create routes collector. + routes := &Routes{ + maxRoutes: defaultMaxRouteMatches, + randomizeTopPercent: defaultRandomizeRoutesTopPercent, + } + + // TODO: + // Start from the destination and use HopDistance to prioritize + // exploring routes that are in the right direction. + // How would we handle selecting the destination node based on route to client? + // Should we just try all destinations? + + // Create initial route. + route := &Route{ + // Estimate how much space we will need, else it'll just expand. + Path: make([]*Hop, 1, routingProfile.MinHops+routingProfile.MaxExtraHops), + } + route.Path[0] = &Hop{ + pin: m.home, + // TODO: add initial cost + } + + // exploreHop explores a hop (Lane) to a connected Pin. + var exploreHop func(route *Route, lane *Lane) + + // exploreLanes explores all Lanes of a Pin. + exploreLanes := func(route *Route) { + for _, lane := range route.Path[len(route.Path)-1].pin.ConnectedTo { + // Check if we are done and can skip the rest. + if done { + return + } + + // Explore! + exploreHop(route, lane) + } + } + + exploreHop = func(route *Route, lane *Lane) { + // Check if the Pin should be regarded as Transit Hub. + if !transitMatcher(lane.Pin) { + return + } + + // Add Pin to the current path and remove when done. + route.addHop(lane.Pin, lane.Cost+lane.Pin.Cost) + defer route.removeHop() + + // Check if the route would even make it into the list. + if !routes.isGoodEnough(route) { + return + } + + // Check route compliance. + // This also includes some algorithm-based optimizations. + switch routingProfile.checkRouteCompliance(route, routes) { + case routeOk: + // Route would be compliant. + // Now, check if the last hop qualifies as a Destination Hub. + if destinationMatcher(lane.Pin) { + // Get Pin as nearby Pin. + nbPin := dsts.get(lane.Pin.Hub.ID) + if nbPin != nil { + // Pin is listed as selected Destination Hub! + // Complete route to add destination ("last mile") cost. + route.completeRoute(nbPin.cost) + routes.add(route) + + // We have found a route and have come to an end here. + return + } + } + + // The Route is compliant, but we haven't found a Destination Hub yet. + fallthrough + case routeNonCompliant: + // Continue exploration. + exploreLanes(route) + case routeDisqualified: + fallthrough + default: + // Route is disqualified and we can return without further exploration. + } + } + + // Start the hop exploration tree. + // This will fork into about a gazillion branches and add all the found valid + // routes to the list. + exploreLanes(route) + + // Check if we found anything. + if len(routes.All) == 0 { + return nil, errors.New("failed to find any routes") + } + + // Randomize top routes for load balancing. + routes.randomizeTop() + + // Copy remaining data to routes. + routes.makeExportReady(opts.RoutingProfile) + + // Debugging: + // log.Debug("spn/navigator: routes:") + // for _, route := range routes.All { + // log.Debugf("spn/navigator: %s", route) + // } + + return routes, nil +} diff --git a/spn/navigator/findroutes_test.go b/spn/navigator/findroutes_test.go new file mode 100644 index 00000000..ed7793c1 --- /dev/null +++ b/spn/navigator/findroutes_test.go @@ -0,0 +1,54 @@ +package navigator + +import ( + "net" + "testing" +) + +func TestFindRoutes(t *testing.T) { + t.Parallel() + + // Create map and lock faking in order to guarantee reproducability of faked data. + m := getOptimizedDefaultTestMap(t) + fakeLock.Lock() + defer fakeLock.Unlock() + + for i := 0; i < 1; i++ { + // Create a random destination address + dstIP, _ := createGoodIP(i%2 == 0) + + routes, err := m.FindRoutes(dstIP, m.DefaultOptions()) + switch { + case err != nil: + t.Error(err) + case len(routes.All) == 0: + t.Logf("No routes for %s", dstIP) + default: + t.Logf("Best route for %s: %s", dstIP, routes.All[0]) + } + } +} + +func BenchmarkFindRoutes(b *testing.B) { + // Create map and lock faking in order to guarantee reproducability of faked data. + m := getOptimizedDefaultTestMap(nil) + fakeLock.Lock() + defer fakeLock.Unlock() + + // Pre-generate 100 IPs + preGenIPs := make([]net.IP, 0, 100) + for i := 0; i < cap(preGenIPs); i++ { + ip, _ := createGoodIP(i%2 == 0) + preGenIPs = append(preGenIPs, ip) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + routes, err := m.FindRoutes(preGenIPs[i%len(preGenIPs)], m.DefaultOptions()) + if err != nil { + b.Error(err) + } else { + b.Logf("Best route for %s: %s", preGenIPs[i%len(preGenIPs)], routes.All[0]) + } + } +} diff --git a/spn/navigator/intel.go b/spn/navigator/intel.go new file mode 100644 index 00000000..d26733c1 --- /dev/null +++ b/spn/navigator/intel.go @@ -0,0 +1,222 @@ +package navigator + +import ( + "context" + "errors" + + "golang.org/x/exp/slices" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/service/intel/geoip" + "github.com/safing/portmaster/service/profile/endpoints" + "github.com/safing/portmaster/spn/hub" +) + +// UpdateIntel supplies the map with new intel data. The data is not copied, so +// it must not be modified after being supplied. If the map is empty, the +// bootstrap hubs will be added to the map. +func (m *Map) UpdateIntel(update *hub.Intel, trustNodes []string) error { + // Check if intel data is already parsed. + if update.Parsed() == nil { + return errors.New("intel data is not parsed") + } + + m.Lock() + defer m.Unlock() + + // Update the map's reference to the intel data. + m.intel = update + + // Update pins with new intel data. + for _, pin := range m.all { + // Add/Update location data from IP addresses. + pin.updateLocationData() + + // Override Pin Data. + m.updateInfoOverrides(pin) + + // Update Trust and Advisory Statuses. + m.updateIntelStatuses(pin, trustNodes) + + // Push changes. + // TODO: Only set when pin changed. + pin.pushChanges.Set() + } + + // Configure the map's regions. + m.updateRegions(m.intel.Regions) + + // Push pin changes. + m.PushPinChanges() + + log.Infof("spn/navigator: updated intel on map %s", m.Name) + + // Add bootstrap hubs if map is empty. + if m.isEmpty() { + return m.addBootstrapHubs(m.intel.BootstrapHubs) + } + return nil +} + +// GetIntel returns the map's intel data. +func (m *Map) GetIntel() *hub.Intel { + m.RLock() + defer m.RUnlock() + + return m.intel +} + +func (m *Map) updateIntelStatuses(pin *Pin, trustNodes []string) { + // Reset all related states. + pin.removeStates(StateTrusted | StateUsageDiscouraged | StateUsageAsHomeDiscouraged | StateUsageAsDestinationDiscouraged) + + // Check if Intel data is loaded. + if m.intel == nil { + return + } + + // Check Hub Intel + hubIntel, ok := m.intel.Hubs[pin.Hub.ID] + if ok { + // Apply the verified owner, if any. + pin.VerifiedOwner = hubIntel.VerifiedOwner + + // Check if Hub is discontinued. + if hubIntel.Discontinued { + // Reset state, set offline and return. + pin.State = StateNone + pin.addStates(StateOffline) + return + } + + // Check if Hub is trusted. + if hubIntel.Trusted { + pin.addStates(StateTrusted) + } + } + + // Check manual trust status. + switch { + case slices.Contains[[]string, string](trustNodes, pin.VerifiedOwner): + pin.addStates(StateTrusted) + case slices.Contains[[]string, string](trustNodes, pin.Hub.ID): + pin.addStates(StateTrusted) + } + + // Check advisories. + // Check for UsageDiscouraged. + checkStatusList( + pin, + StateUsageDiscouraged, + m.intel.AdviseOnlyTrustedHubs, + m.intel.Parsed().HubAdvisory, + ) + // Check for UsageAsHomeDiscouraged. + checkStatusList( + pin, + StateUsageAsHomeDiscouraged, + m.intel.AdviseOnlyTrustedHomeHubs, + m.intel.Parsed().HomeHubAdvisory, + ) + // Check for UsageAsDestinationDiscouraged. + checkStatusList( + pin, + StateUsageAsDestinationDiscouraged, + m.intel.AdviseOnlyTrustedDestinationHubs, + m.intel.Parsed().DestinationHubAdvisory, + ) +} + +func checkStatusList(pin *Pin, state PinState, requireTrusted bool, endpointList endpoints.Endpoints) { + if requireTrusted && !pin.State.Has(StateTrusted) { + pin.addStates(state) + return + } + + if pin.EntityV4 != nil { + result, _ := endpointList.Match(context.TODO(), pin.EntityV4) + if result == endpoints.Denied { + pin.addStates(state) + return + } + } + + if pin.EntityV6 != nil { + result, _ := endpointList.Match(context.TODO(), pin.EntityV6) + if result == endpoints.Denied { + pin.addStates(state) + } + } +} + +func (m *Map) updateInfoOverrides(pin *Pin) { + // Check if Intel data is loaded and if there are any overrides. + if m.intel == nil { + return + } + + // Get overrides for this pin. + hubIntel, ok := m.intel.Hubs[pin.Hub.ID] + if !ok || hubIntel.Override == nil { + return + } + overrides := hubIntel.Override + + // Apply overrides + if overrides.CountryCode != "" { + if pin.LocationV4 != nil { + pin.LocationV4.Country = geoip.GetCountryInfo(overrides.CountryCode) + } + if pin.EntityV4 != nil { + pin.EntityV4.Country = overrides.CountryCode + } + if pin.LocationV6 != nil { + pin.LocationV6.Country = geoip.GetCountryInfo(overrides.CountryCode) + } + if pin.EntityV6 != nil { + pin.EntityV6.Country = overrides.CountryCode + } + } + if overrides.Coordinates != nil { + if pin.LocationV4 != nil { + pin.LocationV4.Coordinates = *overrides.Coordinates + } + if pin.EntityV4 != nil { + pin.EntityV4.Coordinates = overrides.Coordinates + } + if pin.LocationV6 != nil { + pin.LocationV6.Coordinates = *overrides.Coordinates + } + if pin.EntityV6 != nil { + pin.EntityV6.Coordinates = overrides.Coordinates + } + } + if overrides.ASN != 0 { + if pin.LocationV4 != nil { + pin.LocationV4.AutonomousSystemNumber = overrides.ASN + } + if pin.EntityV4 != nil { + pin.EntityV4.ASN = overrides.ASN + } + if pin.LocationV6 != nil { + pin.LocationV6.AutonomousSystemNumber = overrides.ASN + } + if pin.EntityV6 != nil { + pin.EntityV6.ASN = overrides.ASN + } + } + if overrides.ASOrg != "" { + if pin.LocationV4 != nil { + pin.LocationV4.AutonomousSystemOrganization = overrides.ASOrg + } + if pin.EntityV4 != nil { + pin.EntityV4.ASOrg = overrides.ASOrg + } + if pin.LocationV6 != nil { + pin.LocationV6.AutonomousSystemOrganization = overrides.ASOrg + } + if pin.EntityV6 != nil { + pin.EntityV6.ASOrg = overrides.ASOrg + } + } +} diff --git a/spn/navigator/map.go b/spn/navigator/map.go new file mode 100644 index 00000000..006dfc13 --- /dev/null +++ b/spn/navigator/map.go @@ -0,0 +1,165 @@ +package navigator + +import ( + "sort" + "sync" + "time" + + "github.com/safing/portbase/database" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/service/intel/geoip" + "github.com/safing/portmaster/spn/docks" + "github.com/safing/portmaster/spn/hub" +) + +// Map represent a collection of Pins and their relationship and status. +type Map struct { + sync.RWMutex + Name string + + all map[string]*Pin + intel *hub.Intel + regions []*Region + + home *Pin + homeTerminal *docks.CraneTerminal + + measuringEnabled bool + hubUpdateHook *database.RegisteredHook + + // analysisLock guards access to all of this map's Pin.analysis, + // regardedPins and the lastDesegrationAttempt fields. + analysisLock sync.Mutex + regardedPins []*Pin + lastDesegrationAttempt time.Time +} + +// NewMap returns a new and empty Map. +func NewMap(name string, enableMeasuring bool) *Map { + m := &Map{ + Name: name, + all: make(map[string]*Pin), + measuringEnabled: enableMeasuring, + } + addMapToAPI(m) + + return m +} + +// Close removes the map's integration, taking it "offline". +func (m *Map) Close() { + removeMapFromAPI(m.Name) +} + +// GetPin returns the Pin of the Hub with the given ID. +func (m *Map) GetPin(hubID string) (pin *Pin, ok bool) { + m.RLock() + defer m.RUnlock() + + pin, ok = m.all[hubID] + return +} + +// GetHome returns the current home and it's accompanying terminal. +// Both may be nil. +func (m *Map) GetHome() (*Pin, *docks.CraneTerminal) { + m.RLock() + defer m.RUnlock() + + return m.home, m.homeTerminal +} + +// SetHome sets the given hub as the new home. Optionally, a terminal may be +// supplied to accompany the home hub. +func (m *Map) SetHome(id string, t *docks.CraneTerminal) (ok bool) { + m.Lock() + defer m.Unlock() + + // Get pin from map. + newHome, ok := m.all[id] + if !ok { + return false + } + + // Remove home hub state from all pins. + for _, pin := range m.all { + pin.removeStates(StateIsHomeHub) + } + + // Set pin as home. + m.home = newHome + m.homeTerminal = t + m.home.addStates(StateIsHomeHub) + + // Recalculate reachable. + err := m.recalculateReachableHubs() + if err != nil { + log.Warningf("spn/navigator: failed to recalculate reachable hubs: %s", err) + } + + m.PushPinChanges() + return true +} + +// GetAvailableCountries returns a map of countries including their information +// where the map has pins suitable for the given type. +func (m *Map) GetAvailableCountries(opts *Options, forType HubType) map[string]*geoip.CountryInfo { + if opts == nil { + opts = m.defaultOptions() + } + + m.RLock() + defer m.RUnlock() + + matcher := opts.Matcher(forType, m.intel) + countries := make(map[string]*geoip.CountryInfo) + for _, pin := range m.all { + if !matcher(pin) { + continue + } + if pin.LocationV4 != nil && countries[pin.LocationV4.Country.Code] == nil { + countries[pin.LocationV4.Country.Code] = &pin.LocationV4.Country + } + if pin.LocationV6 != nil && countries[pin.LocationV6.Country.Code] == nil { + countries[pin.LocationV6.Country.Code] = &pin.LocationV6.Country + } + } + + return countries +} + +// isEmpty returns whether the Map is regarded as empty. +func (m *Map) isEmpty() bool { + if m.home != nil { + // When a home hub is set, we also regard a map with only one entry to be + // empty, as this will be the case for Hubs, which will have their own + // entry in the Map. + return len(m.all) <= 1 + } + + return len(m.all) == 0 +} + +func (m *Map) pinList(lockMap bool) []*Pin { + if lockMap { + m.RLock() + defer m.RUnlock() + } + + // Copy into slice. + list := make([]*Pin, 0, len(m.all)) + for _, pin := range m.all { + list = append(list, pin) + } + + return list +} + +func (m *Map) sortedPins(lockMap bool) []*Pin { + // Get list. + list := m.pinList(lockMap) + + // Sort list. + sort.Sort(sortByPinID(list)) + return list +} diff --git a/spn/navigator/map_stats.go b/spn/navigator/map_stats.go new file mode 100644 index 00000000..c4e17108 --- /dev/null +++ b/spn/navigator/map_stats.go @@ -0,0 +1,85 @@ +package navigator + +import ( + "fmt" + "sort" + "strings" +) + +// MapStats holds generic map statistics. +type MapStats struct { + Name string + States map[PinState]int + Lanes map[int]int + ActiveTerminals int +} + +// Stats collects and returns statistics from the map. +func (m *Map) Stats() *MapStats { + m.Lock() + defer m.Unlock() + + // Create stats struct. + stats := &MapStats{ + Name: m.Name, + States: make(map[PinState]int), + Lanes: make(map[int]int), + } + for _, state := range allStates { + stats.States[state] = 0 + } + + // Iterate over all Pins to collect data. + for _, pin := range m.all { + // Count active terminals. + if pin.HasActiveTerminal() { + stats.ActiveTerminals++ + } + + // Check all states. + for _, state := range allStates { + if pin.State.Has(state) { + stats.States[state]++ + } + } + + // Count lanes. + laneCnt, ok := stats.Lanes[len(pin.ConnectedTo)] + if ok { + stats.Lanes[len(pin.ConnectedTo)] = laneCnt + 1 + } else { + stats.Lanes[len(pin.ConnectedTo)] = 1 + } + } + + return stats +} + +func (ms *MapStats) String() string { + var builder strings.Builder + + // Write header. + fmt.Fprintf(&builder, "Stats for Map %s:\n", ms.Name) + + // Write State Stats + stateSummary := make([]string, 0, len(ms.States)) + for state, cnt := range ms.States { + stateSummary = append(stateSummary, fmt.Sprintf("State %s: %d Hubs", state, cnt)) + } + sort.Strings(stateSummary) + for _, stateSum := range stateSummary { + fmt.Fprintln(&builder, stateSum) + } + + // Write Lane Stats + laneStats := make([]string, 0, len(ms.Lanes)) + for laneCnt, pinCnt := range ms.Lanes { + laneStats = append(laneStats, fmt.Sprintf("%d Lanes: %d Hubs", laneCnt, pinCnt)) + } + sort.Strings(laneStats) + for _, laneStat := range laneStats { + fmt.Fprintln(&builder, laneStat) + } + + return builder.String() +} diff --git a/spn/navigator/map_test.go b/spn/navigator/map_test.go new file mode 100644 index 00000000..bea2d477 --- /dev/null +++ b/spn/navigator/map_test.go @@ -0,0 +1,279 @@ +package navigator + +import ( + "fmt" + "net" + "sync" + "testing" + "time" + + "github.com/brianvoe/gofakeit" + + "github.com/safing/jess/lhash" + "github.com/safing/portmaster/service/intel/geoip" + "github.com/safing/portmaster/spn/hub" +) + +var ( + fakeLock sync.Mutex + + defaultMapCreate sync.Once + defaultMap *Map +) + +func getDefaultTestMap() *Map { + defaultMapCreate.Do(func() { + defaultMap = createRandomTestMap(1, 200) + }) + return defaultMap +} + +func TestRandomMapCreation(t *testing.T) { + t.Parallel() + + m := getDefaultTestMap() + + fmt.Println("All Pins:") + for _, pin := range m.all { + fmt.Printf("%s: %s %s\n", pin, pin.Hub.Info.IPv4, pin.Hub.Info.IPv6) + } + + // Print stats + fmt.Printf("\n%s\n", m.Stats()) + + // Print home + fmt.Printf("Selected Home Hub: %s\n", m.home) +} + +func createRandomTestMap(seed int64, size int) *Map { + fakeLock.Lock() + defer fakeLock.Unlock() + + // Seed with parameter to make it reproducible. + gofakeit.Seed(seed) + + // Enforce minimum size. + if size < 10 { + size = 10 + } + + // Create Hub list. + var hubs []*hub.Hub + + // Create Intel data structure. + mapIntel := &hub.Intel{ + Hubs: make(map[string]*hub.HubIntel), + } + + // Define periodic values. + var currentGroup string + + // Create [size] fake Hubs. + for i := 0; i < size; i++ { + // Change group every 5 Hubs. + if i%5 == 0 { + currentGroup = gofakeit.Username() + } + + // Create new fake Hub and add to the list. + h := createFakeHub(currentGroup, true, mapIntel) + hubs = append(hubs, h) + } + + // Fake three superseeded Hubs. + for i := 0; i < 3; i++ { + h := hubs[size-1-i] + + // Set FirstSeen in the past and copy an IP address of an existing Hub. + h.FirstSeen = time.Now().Add(-1 * time.Hour) + if i%2 == 0 { + h.Info.IPv4 = hubs[i].Info.IPv4 + } else { + h.Info.IPv6 = hubs[i].Info.IPv6 + } + } + + // Create Lanes between Hubs in order to create the network. + totalConnections := size * 10 + for i := 0; i < totalConnections; i++ { + // Get new random indexes. + indexA := gofakeit.Number(0, size-1) + indexB := gofakeit.Number(0, size-1) + if indexA == indexB { + continue + } + + // Get Hubs and check if they are already connected. + hubA := hubs[indexA] + hubB := hubs[indexB] + if hubA.GetLaneTo(hubB.ID) != nil { + // already connected + continue + } + if hubB.GetLaneTo(hubA.ID) != nil { + // already connected + continue + } + + // Create connections. + _ = hubA.AddLane(createLane(hubB.ID)) + // Add the second connection in 99% of cases. + // If this is missing, the Pins should not show up as connected. + if gofakeit.Number(0, 100) != 0 { + _ = hubB.AddLane(createLane(hubA.ID)) + } + } + + // Parse constructed intel data + err := mapIntel.ParseAdvisories() + if err != nil { + panic(err) + } + + // Create map and add Pins. + m := NewMap(fmt.Sprintf("Test-Map-%d", seed), true) + m.intel = mapIntel + for _, h := range hubs { + m.UpdateHub(h) + } + + // Fake communication error with three Hubs. + var i int + for _, pin := range m.all { + pin.MarkAsFailingFor(1 * time.Hour) + pin.addStates(StateFailing) + + if i++; i >= 3 { + break + } + } + + // Set a Home Hub. + findFakeHomeHub(m) + + return m +} + +func createFakeHub(group string, randomFailes bool, mapIntel *hub.Intel) *hub.Hub { + // Create fake Hub ID. + idSrc := gofakeit.Password(true, true, true, true, true, 64) + id := lhash.Digest(lhash.BLAKE2b_256, []byte(idSrc)).Base58() + ip4, _ := createGoodIP(true) + ip6, _ := createGoodIP(false) + + // Create and return new fake Hub. + h := &hub.Hub{ + ID: id, + Info: &hub.Announcement{ + ID: id, + Timestamp: time.Now().Unix(), + Name: gofakeit.Username(), + Group: group, + // ContactAddress // TODO + // ContactService // TODO + // Hosters []string // TODO + // Datacenter string // TODO + IPv4: ip4, + IPv6: ip6, + }, + Status: &hub.Status{ + Timestamp: time.Now().Unix(), + Keys: map[string]*hub.Key{ + "a": { + Expires: time.Now().Add(48 * time.Hour).Unix(), + }, + }, + Load: gofakeit.Number(10, 100), + }, + Measurements: hub.NewMeasurements(), + FirstSeen: time.Now(), + } + h.Measurements.Latency = createLatency() + h.Measurements.Capacity = createCapacity() + h.Measurements.CalculatedCost = CalculateLaneCost( + h.Measurements.Latency, + h.Measurements.Capacity, + ) + + // Return if not failures of any kind should be simulated. + if !randomFailes { + return h + } + + // Set hub-based states. + if gofakeit.Number(0, 100) == 0 { + // Fake Info message error. + h.InvalidInfo = true + } + if gofakeit.Number(0, 100) == 0 { + // Fake Status message error. + h.InvalidStatus = true + } + if gofakeit.Number(0, 100) == 0 { + // Fake expired exchange keys. + for _, key := range h.Status.Keys { + key.Expires = time.Now().Add(-1 * time.Hour).Unix() + } + } + + // Return if not failures of any kind should be simulated. + if mapIntel == nil { + return h + } + + // Set advisory-based states. + if gofakeit.Number(0, 10) == 0 { + // Make Trusted State + mapIntel.Hubs[h.ID] = &hub.HubIntel{ + Trusted: true, + } + } + if gofakeit.Number(0, 100) == 0 { + // Discourage any usage. + mapIntel.HubAdvisory = append(mapIntel.HubAdvisory, "- "+h.Info.IPv4.String()) + } + if gofakeit.Number(0, 100) == 0 { + // Discourage Home Hub usage. + mapIntel.HomeHubAdvisory = append(mapIntel.HomeHubAdvisory, "- "+h.Info.IPv4.String()) + } + if gofakeit.Number(0, 100) == 0 { + // Discourage Destination Hub usage. + mapIntel.DestinationHubAdvisory = append(mapIntel.DestinationHubAdvisory, "- "+h.Info.IPv4.String()) + } + + return h +} + +func createGoodIP(v4 bool) (net.IP, *geoip.Location) { + var candidate net.IP + for i := 0; i < 100; i++ { + if v4 { + candidate = net.ParseIP(gofakeit.IPv4Address()) + } else { + candidate = net.ParseIP(gofakeit.IPv6Address()) + } + loc, err := geoip.GetLocation(candidate) + if err == nil && loc.Coordinates.Latitude != 0 { + return candidate, loc + } + } + return candidate, nil +} + +func createLane(toHubID string) *hub.Lane { + return &hub.Lane{ + ID: toHubID, + Latency: createLatency(), + Capacity: createCapacity(), + } +} + +func createLatency() time.Duration { + // Return a value between 10ms and 100ms. + return time.Duration(gofakeit.Float64Range(10, 100) * float64(time.Millisecond)) +} + +func createCapacity() int { + // Return a value between 10Mbit/s and 1Gbit/s. + return gofakeit.Number(10000000, 1000000000) +} diff --git a/spn/navigator/measurements.go b/spn/navigator/measurements.go new file mode 100644 index 00000000..571365cb --- /dev/null +++ b/spn/navigator/measurements.go @@ -0,0 +1,144 @@ +package navigator + +import ( + "context" + "sort" + "time" + + "github.com/safing/portbase/log" + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/spn/docks" + "github.com/safing/portmaster/spn/terminal" +) + +// Measurements Configuration. +const ( + NavigatorMeasurementTTLDefault = 4 * time.Hour + NavigatorMeasurementTTLByCostBase = 6 * time.Minute + NavigatorMeasurementTTLByCostMin = 4 * time.Hour + NavigatorMeasurementTTLByCostMax = 50 * time.Hour + + // With a base TTL of 3m, this leads to: + // 20c -> 2h -> raised to 4h. + // 50c -> 5h + // 100c -> 10h + // 1000c -> 100h -> capped to 50h. +) + +func (m *Map) measureHubs(ctx context.Context, _ *modules.Task) error { + if home, _ := m.GetHome(); home == nil { + log.Debug("spn/navigator: skipping measuring, no home hub set") + return nil + } + + var unknownErrCnt int + matcher := m.DefaultOptions().Transit.Matcher(m.GetIntel()) + + // Get list and sort in order to check near/low-cost hubs earlier. + list := m.pinList(true) + sort.Sort(sortByLowestMeasuredCost(list)) + + // Find first pin where any measurement has expired. + for _, pin := range list { + // Check if measuring is enabled. + if pin.measurements == nil { + continue + } + + // Check if Pin is regarded. + if !matcher(pin) { + continue + } + + // Calculate dynamic TTL. + var checkWithTTL time.Duration + if pin.HopDistance == 2 { // Hub is directly connected. + checkWithTTL = calculateMeasurementTTLByCost( + pin.measurements.GetCalculatedCost(), + docks.CraneMeasurementTTLByCostBase, + docks.CraneMeasurementTTLByCostMin, + docks.CraneMeasurementTTLByCostMax, + ) + } else { + checkWithTTL = calculateMeasurementTTLByCost( + pin.measurements.GetCalculatedCost(), + NavigatorMeasurementTTLByCostBase, + NavigatorMeasurementTTLByCostMin, + NavigatorMeasurementTTLByCostMax, + ) + } + + // Check if we have measured the pin within the TTL. + if !pin.measurements.Expired(checkWithTTL) { + continue + } + + // Measure connection. + tErr := docks.MeasureHub(ctx, pin.Hub, checkWithTTL) + + // Independent of outcome, recalculate the cost. + latency, _ := pin.measurements.GetLatency() + capacity, _ := pin.measurements.GetCapacity() + calculatedCost := CalculateLaneCost(latency, capacity) + pin.measurements.SetCalculatedCost(calculatedCost) + // Log result. + log.Infof( + "spn/navigator: updated measurements for connection to %s: %s %.2fMbit/s %.2fc", + pin.Hub, + latency, + float64(capacity)/1000000, + calculatedCost, + ) + + switch { + case tErr.IsOK(): + // All good, continue. + + case tErr.Is(terminal.ErrTryAgainLater): + if tErr.IsExternal() { + // Remote is measuring, just continue with next. + log.Debugf("spn/navigator: remote %s is measuring, continuing with next", pin.Hub) + } else { + // We are measuring, abort and restart measuring again later. + log.Debugf("spn/navigator: postponing measuring because we are currently engaged in measuring") + return nil + } + + default: + log.Warningf("spn/navigator: failed to measure connection to %s: %s", pin.Hub, tErr) + unknownErrCnt++ + if unknownErrCnt >= 3 { + log.Warningf("spn/navigator: postponing measuring task because of multiple errors") + return nil + } + } + } + + return nil +} + +// SaveMeasuredHubs saves all Hubs that have unsaved measurements. +func (m *Map) SaveMeasuredHubs() { + m.RLock() + defer m.RUnlock() + + for _, pin := range m.all { + if !pin.measurements.IsPersisted() { + if err := pin.Hub.Save(); err != nil { + log.Warningf("spn/navigator: failed to save Hub %s to persist measurements: %s", pin.Hub, err) + } + } + } +} + +func calculateMeasurementTTLByCost(cost float32, base, min, max time.Duration) time.Duration { + calculated := time.Duration(cost) * base + switch { + case calculated < min: + return min + case calculated > max: + return max + default: + return calculated + } +} diff --git a/spn/navigator/metrics.go b/spn/navigator/metrics.go new file mode 100644 index 00000000..fe62020e --- /dev/null +++ b/spn/navigator/metrics.go @@ -0,0 +1,177 @@ +package navigator + +import ( + "sort" + "sync" + "time" + + "github.com/tevino/abool" + + "github.com/safing/portbase/api" + "github.com/safing/portbase/metrics" +) + +var metricsRegistered = abool.New() + +func registerMetrics() (err error) { + // Only register metrics once. + if !metricsRegistered.SetToIf(false, true) { + return nil + } + + // Map Stats. + + _, err = metrics.NewGauge( + "spn/map/main/latency/all/lowest/seconds", + nil, + getLowestLatency, + &metrics.Options{ + Name: "SPN Map Lowest Latency", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + _, err = metrics.NewGauge( + "spn/map/main/latency/fas/lowest/seconds", + nil, + getLowestLatencyFromFas, + &metrics.Options{ + Name: "SPN Map Lowest Latency", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + _, err = metrics.NewGauge( + "spn/map/main/capacity/all/highest/bytes", + nil, + getHighestCapacity, + &metrics.Options{ + Name: "SPN Map Lowest Latency", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + _, err = metrics.NewGauge( + "spn/map/main/capacity/fas/highest/bytes", + nil, + getHighestCapacityFromFas, + &metrics.Options{ + Name: "SPN Map Lowest Latency", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + return nil +} + +var ( + mapStats *mapMetrics + mapStatsExpires time.Time + mapStatsLock sync.Mutex + mapStatsTTL = 55 * time.Second +) + +type mapMetrics struct { + lowestLatency float64 + lowestForeignASLatency float64 + highestCapacity float64 + highestForeignASCapacity float64 +} + +func getLowestLatency() float64 { return getMapStats().lowestLatency } +func getLowestLatencyFromFas() float64 { return getMapStats().lowestForeignASLatency } +func getHighestCapacity() float64 { return getMapStats().highestCapacity } +func getHighestCapacityFromFas() float64 { return getMapStats().highestForeignASCapacity } + +func getMapStats() *mapMetrics { + mapStatsLock.Lock() + defer mapStatsLock.Unlock() + + // Return cache if still valid. + if time.Now().Before(mapStatsExpires) { + return mapStats + } + + // Refresh. + mapStats = &mapMetrics{} + + // Get all pins and home. + list := Main.pinList(true) + home, _ := Main.GetHome() + + // Return empty stats if we have incomplete data. + if len(list) <= 1 || home == nil { + mapStatsExpires = time.Now().Add(mapStatsTTL) + return mapStats + } + + // Sort by latency. + sort.Sort(sortByLowestMeasuredLatency(list)) + // Get lowest latency. + lowestLatency, _ := list[0].measurements.GetLatency() + mapStats.lowestLatency = lowestLatency.Seconds() + // Find best foreign AS latency. + bestForeignASPin := findFirstForeignASStatsPin(home, list) + if bestForeignASPin != nil { + lowestForeignASLatency, _ := bestForeignASPin.measurements.GetLatency() + mapStats.lowestForeignASLatency = lowestForeignASLatency.Seconds() + } + + // Sort by capacity. + sort.Sort(sortByHighestMeasuredCapacity(list)) + // Get highest capacity. + highestCapacity, _ := list[0].measurements.GetCapacity() + mapStats.highestCapacity = float64(highestCapacity) / 8 + // Find best foreign AS capacity. + bestForeignASPin = findFirstForeignASStatsPin(home, list) + if bestForeignASPin != nil { + highestForeignASCapacity, _ := bestForeignASPin.measurements.GetCapacity() + mapStats.highestForeignASCapacity = float64(highestForeignASCapacity) / 8 + } + + mapStatsExpires = time.Now().Add(mapStatsTTL) + return mapStats +} + +func findFirstForeignASStatsPin(home *Pin, list []*Pin) *Pin { + // Find best foreign AS latency. + for _, pin := range list { + compared := false + + // Skip if IPv4 AS matches. + if home.LocationV4 != nil && pin.LocationV4 != nil { + if home.LocationV4.AutonomousSystemNumber == pin.LocationV4.AutonomousSystemNumber { + continue + } + compared = true + } + + // Skip if IPv6 AS matches. + if home.LocationV6 != nil && pin.LocationV6 != nil { + if home.LocationV6.AutonomousSystemNumber == pin.LocationV6.AutonomousSystemNumber { + continue + } + compared = true + } + + // Skip if no data was compared + if !compared { + continue + } + + return pin + } + return nil +} diff --git a/spn/navigator/module.go b/spn/navigator/module.go new file mode 100644 index 00000000..9937ad61 --- /dev/null +++ b/spn/navigator/module.go @@ -0,0 +1,129 @@ +package navigator + +import ( + "errors" + "time" + + "github.com/safing/portbase/config" + "github.com/safing/portbase/log" + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/service/intel/geoip" + "github.com/safing/portmaster/spn/conf" +) + +const ( + // cfgOptionRoutingAlgorithmKey is copied from profile/config.go to avoid import loop. + cfgOptionRoutingAlgorithmKey = "spn/routingAlgorithm" + + // cfgOptionRoutingAlgorithmKey is copied from captain/config.go to avoid import loop. + cfgOptionTrustNodeNodesKey = "spn/trustNodes" +) + +var ( + // ErrHomeHubUnset is returned when the Home Hub is required and not set. + ErrHomeHubUnset = errors.New("map has no Home Hub set") + + // ErrEmptyMap is returned when the Map is empty. + ErrEmptyMap = errors.New("map is empty") + + // ErrHubNotFound is returned when the Hub was not found on the Map. + ErrHubNotFound = errors.New("hub not found") + + // ErrAllPinsDisregarded is returned when all pins have been disregarded. + ErrAllPinsDisregarded = errors.New("all pins have been disregarded") +) + +var ( + module *modules.Module + + // Main is the primary map used. + Main *Map + + devMode config.BoolOption + cfgOptionRoutingAlgorithm config.StringOption + cfgOptionTrustNodeNodes config.StringArrayOption +) + +func init() { + module = modules.Register("navigator", prep, start, stop, "terminal", "geoip", "netenv") +} + +func prep() error { + return registerAPIEndpoints() +} + +func start() error { + Main = NewMap(conf.MainMapName, true) + devMode = config.Concurrent.GetAsBool(config.CfgDevModeKey, false) + cfgOptionRoutingAlgorithm = config.Concurrent.GetAsString(cfgOptionRoutingAlgorithmKey, DefaultRoutingProfileID) + cfgOptionTrustNodeNodes = config.Concurrent.GetAsStringArray(cfgOptionTrustNodeNodesKey, []string{}) + + err := registerMapDatabase() + if err != nil { + return err + } + + // Wait for geoip databases to be ready. + // Try again if not yet ready, as this is critical. + // The "wait" parameter times out after 1 second. + // Allow 30 seconds for both databases to load. +geoInitCheck: + for i := 0; i < 30; i++ { + switch { + case !geoip.IsInitialized(false, true): // First, IPv4. + case !geoip.IsInitialized(true, true): // Then, IPv6. + default: + break geoInitCheck + } + } + + err = Main.InitializeFromDatabase() + if err != nil { + // Wait for three seconds, then try again. + time.Sleep(3 * time.Second) + err = Main.InitializeFromDatabase() + if err != nil { + // Even if the init fails, we can try to start without it and get data along the way. + log.Warningf("spn/navigator: %s", err) + } + } + err = Main.RegisterHubUpdateHook() + if err != nil { + return err + } + + // TODO: delete superseded hubs after x amount of time + + module.NewTask("update states", Main.updateStates). + Repeat(1 * time.Hour). + Schedule(time.Now().Add(3 * time.Minute)) + + module.NewTask("update failing states", Main.updateFailingStates). + Repeat(1 * time.Minute). + Schedule(time.Now().Add(3 * time.Minute)) + + if conf.PublicHub() { + // Only measure Hubs on public Hubs. + module.NewTask("measure hubs", Main.measureHubs). + Repeat(5 * time.Minute). + Schedule(time.Now().Add(1 * time.Minute)) + + // Only register metrics on Hubs, as they only make sense there. + err := registerMetrics() + if err != nil { + return err + } + } + + return nil +} + +func stop() error { + withdrawMapDatabase() + + Main.CancelHubUpdateHook() + Main.SaveMeasuredHubs() + Main.Close() + + return nil +} diff --git a/spn/navigator/module_test.go b/spn/navigator/module_test.go new file mode 100644 index 00000000..f55ea4e8 --- /dev/null +++ b/spn/navigator/module_test.go @@ -0,0 +1,13 @@ +package navigator + +import ( + "testing" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/service/core/pmtesting" +) + +func TestMain(m *testing.M) { + log.SetLogLevel(log.DebugLevel) + pmtesting.TestMain(m, module) +} diff --git a/spn/navigator/optimize.go b/spn/navigator/optimize.go new file mode 100644 index 00000000..76f101c3 --- /dev/null +++ b/spn/navigator/optimize.go @@ -0,0 +1,388 @@ +package navigator + +import ( + "fmt" + "sort" + "time" + + "github.com/safing/portmaster/spn/docks" + "github.com/safing/portmaster/spn/hub" +) + +const ( + optimizationLowestCostConnections = 3 + optimizationHopDistanceTarget = 3 + waitUntilMeasuredUpToPercent = 0.5 + + desegrationAttemptBackoff = time.Hour +) + +// Optimization Purposes. +const ( + OptimizePurposeBootstrap = "bootstrap" + OptimizePurposeDesegregate = "desegregate" + OptimizePurposeWait = "wait" + OptimizePurposeTargetStructure = "target-structure" +) + +// AnalysisState holds state for analyzing the network for optimizations. +type AnalysisState struct { //nolint:maligned + // Suggested signifies that a direct connection to this Hub is suggested by + // the optimization algorithm. + Suggested bool + + // SuggestedHopDistance holds the hop distance to this Hub when only + // considering the suggested Hubs as connected. + SuggestedHopDistance int + + // SuggestedHopDistanceInRegion holds the hop distance to this Hub in the + // same region when only considering the suggested Hubs as connected. + SuggestedHopDistanceInRegion int + + // CrossRegionalConnections holds the amount of connections a Pin has from + // the current region. + CrossRegionalConnections int + // CrossRegionalLowestCostLane holds the lowest cost of the counted + // connections from the current region. + CrossRegionalLowestCostLane float32 + // CrossRegionalLaneCosts holds all the cross regional lane costs. + CrossRegionalLaneCosts []float32 + // CrossRegionalHighestCostInHubLimit holds to highest cost of the lowest + // cost connections within the maximum allowed lanes on a Hub from the + // current region. + CrossRegionalHighestCostInHubLimit float32 +} + +// initAnalysis creates all Pin.analysis fields. +// The caller needs to hold the map and analysis lock.. +func (m *Map) initAnalysis(result *OptimizationResult) { + // Compile lists of regarded pins. + m.regardedPins = make([]*Pin, 0, len(m.all)) + for _, region := range m.regions { + region.regardedPins = make([]*Pin, 0, len(m.all)) + } + // Find all regarded pins. + for _, pin := range m.all { + if result.matcher(pin) { + m.regardedPins = append(m.regardedPins, pin) + // Add to region. + if pin.region != nil { + pin.region.regardedPins = append(pin.region.regardedPins, pin) + } + } + } + + // Initialize analysis state. + for _, pin := range m.all { + pin.analysis = &AnalysisState{} + } +} + +// clearAnalysis reset all Pin.analysis fields. +// The caller needs to hold the map and analysis lock. +func (m *Map) clearAnalysis() { + m.regardedPins = nil + for _, region := range m.regions { + region.regardedPins = nil + } + for _, pin := range m.all { + pin.analysis = nil + } +} + +// OptimizationResult holds the result of an optimizaion analysis. +type OptimizationResult struct { + // Purpose holds a semi-human readable constant of the optimization purpose. + Purpose string + + // Approach holds human readable descriptions of how the stated purpose + // should be achieved. + Approach []string + + // SuggestedConnections holds the Hubs to which connections are suggested. + SuggestedConnections []*SuggestedConnection + + // MaxConnect specifies how many connections should be created at maximum + // based on this optimization. + MaxConnect int + + // StopOthers specifies if other connections than the suggested ones may + // be stopped. + StopOthers bool + + // opts holds the options for matching Hubs in this optimization. + opts *HubOptions + + // matcher is the matcher used to create the regarded Pins. + // Required for updating suggested hop distance. + matcher PinMatcher +} + +// SuggestedConnection holds suggestions by the optimization system. +type SuggestedConnection struct { + // Hub holds the Hub to which a connection is suggested. + Hub *hub.Hub + // pin holds the Pin of the Hub. + pin *Pin + // Reason holds a reason why this connection is suggested. + Reason string + // Duplicate marks duplicate entries. These should be ignored when + // connecting, but are helpful for understand the optimization result. + Duplicate bool +} + +func (or *OptimizationResult) addApproach(description string) { + or.Approach = append(or.Approach, description) +} + +func (or *OptimizationResult) addSuggested(reason string, pins ...*Pin) { + for _, pin := range pins { + // Mark as suggested. + pin.analysis.Suggested = true + + // Check if this is a duplicate. + var duplicate bool + for _, sc := range or.SuggestedConnections { + if pin.Hub.ID == sc.Hub.ID { + duplicate = true + break + } + } + + // Add to suggested connections. + or.SuggestedConnections = append(or.SuggestedConnections, &SuggestedConnection{ + Hub: pin.Hub, + pin: pin, + Reason: reason, + Duplicate: duplicate, + }) + + // Update hop distances if we have a matcher. + if or.matcher != nil { + or.markSuggestedReachable(pin, 2) + or.markSuggestedReachableInRegion(pin, 2) + } + } +} + +func (or *OptimizationResult) markSuggestedReachable(suggested *Pin, hopDistance int) { + // Don't update if distance is greater or equal than current one. + if hopDistance >= suggested.analysis.SuggestedHopDistance { + return + } + + // Set suggested hop distance. + suggested.analysis.SuggestedHopDistance = hopDistance + + // Increase distance and apply to matching Pins. + hopDistance++ + for _, lane := range suggested.ConnectedTo { + if or.matcher(lane.Pin) { + or.markSuggestedReachable(lane.Pin, hopDistance) + } + } +} + +// Optimize analyzes the map and suggests changes. +func (m *Map) Optimize(opts *HubOptions) (result *OptimizationResult, err error) { + m.RLock() + defer m.RUnlock() + + // Check if the map is empty. + if m.isEmpty() { + return nil, ErrEmptyMap + } + + // Set default options if unset. + if opts == nil { + opts = &HubOptions{} + } + + return m.optimize(opts) +} + +func (m *Map) optimize(opts *HubOptions) (result *OptimizationResult, err error) { + if m.home == nil { + return nil, ErrHomeHubUnset + } + + // Set default options if unset. + if opts == nil { + opts = &HubOptions{} + } + + // Create result. + result = &OptimizationResult{ + opts: opts, + matcher: opts.Matcher(TransitHub, m.intel), + } + + // Setup analyis. + m.analysisLock.Lock() + defer m.analysisLock.Unlock() + m.initAnalysis(result) + defer m.clearAnalysis() + + // Bootstrap to the network and desegregate map. + // If there is a result, return it immediately. + returnImmediately := m.optimizeForBootstrappingAndDesegregation(result) + if returnImmediately { + return result, nil + } + + // Check if we have the measurements we need. + if m.measuringEnabled { + // Cound pins with valid measurements. + var validMeasurements float32 + for _, pin := range m.regardedPins { + if pin.measurements.Valid() { + validMeasurements++ + } + } + + // If less than the required amount of regarded Pins have valid + // measurements, let's wait until we have that. + if validMeasurements/float32(len(m.regardedPins)) < waitUntilMeasuredUpToPercent { + return &OptimizationResult{ + Purpose: OptimizePurposeWait, + Approach: []string{"Wait for measurements of 80% of regarded nodes for better optimization."}, + }, nil + } + } + + // Set default values for target structure optimization. + result.Purpose = OptimizePurposeTargetStructure + result.MaxConnect = 3 + result.StopOthers = true + + // Optimize for lowest cost. + m.optimizeForLowestCost(result, optimizationLowestCostConnections) + + // Optimize for lowest cost in region. + m.optimizeForLowestCostInRegion(result) + + // Optimize for distance constraint in region. + m.optimizeForDistanceConstraintInRegion(result, 3) + + // Optimize for region-to-region connectivity. + m.optimizeForRegionConnectivity(result) + + // Optimize for satellite-to-region connectivity. + m.optimizeForSatelliteConnectivity(result) + + // Lapse traffic stats after optimizing for good fresh data next time. + for _, crane := range docks.GetAllAssignedCranes() { + crane.NetState.LapsePeriod() + } + + // Clean and return. + return result, nil +} + +func (m *Map) optimizeForBootstrappingAndDesegregation(result *OptimizationResult) (returnImmediately bool) { + // All regarded Pins are reachable. + reachable := len(m.regardedPins) + + // Count Pins that may be connectable. + connectable := make([]*Pin, 0, len(m.all)) + // Copy opts as we are going to make changes. + opts := result.opts.Copy() + opts.NoDefaults = true + opts.Regard = StateNone + opts.Disregard = StateSummaryDisregard + // Collect Pins with matcher. + matcher := opts.Matcher(TransitHub, m.intel) + for _, pin := range m.all { + if matcher(pin) { + connectable = append(connectable, pin) + } + } + + switch { + case reachable == 0: + + // Sort by lowest cost. + sort.Sort(sortByLowestMeasuredCost(connectable)) + + // Return bootstrap optimization. + result.Purpose = OptimizePurposeBootstrap + result.Approach = []string{"Connect to a near Hub to connect to the network."} + result.MaxConnect = 1 + result.addSuggested("bootstrap", connectable...) + return true + + case reachable > len(connectable)/2: + // We are part of the majority network, continue with regular optimization. + + case time.Now().Add(-desegrationAttemptBackoff).Before(m.lastDesegrationAttempt): + // We tried to desegregate recently, continue with regular optimization. + + default: + // We are in a network comprised of less than half of the known nodes. + // Attempt to connect to an unconnected one to desegregate the network. + + // Copy opts as we are going to make changes. + opts = opts.Copy() + opts.NoDefaults = true + opts.Regard = StateNone + opts.Disregard = StateSummaryDisregard | StateReachable + + // Iterate over all Pins to find any matching Pin. + desegregateWith := make([]*Pin, 0, len(m.all)-reachable) + matcher := opts.Matcher(TransitHub, m.intel) + for _, pin := range m.all { + if matcher(pin) { + desegregateWith = append(desegregateWith, pin) + } + } + + // Sort by lowest connection cost. + sort.Sort(sortByLowestMeasuredCost(desegregateWith)) + + // Build desegration optimization. + result.Purpose = OptimizePurposeDesegregate + result.Approach = []string{"Attempt to desegregate network by connection to an unreachable Hub."} + result.MaxConnect = 1 + result.addSuggested("desegregate", desegregateWith...) + + // Record desegregation attempt. + m.lastDesegrationAttempt = time.Now() + + return true + } + + return false +} + +func (m *Map) optimizeForLowestCost(result *OptimizationResult, max int) { + // Add approach. + result.addApproach(fmt.Sprintf("Connect to best (lowest cost) %d Hubs globally.", max)) + + // Sort by lowest cost. + sort.Sort(sortByLowestMeasuredCost(m.regardedPins)) + + // Add to suggested pins. + if len(m.regardedPins) <= max { + result.addSuggested("best globally", m.regardedPins...) + } else { + result.addSuggested("best globally", m.regardedPins[:max]...) + } +} + +func (m *Map) optimizeForDistanceConstraint(result *OptimizationResult, max int) { //nolint:unused // TODO: Likely to be used again. + // Add approach. + result.addApproach(fmt.Sprintf("Satisfy max hop constraint of %d globally.", optimizationHopDistanceTarget)) + + for i := 0; i < max; i++ { + // Sort by lowest cost. + sort.Sort(sortBySuggestedHopDistanceAndLowestMeasuredCost(m.regardedPins)) + + // Return when all regarded Pins are within the distance constraint. + if m.regardedPins[0].analysis.SuggestedHopDistance <= optimizationHopDistanceTarget { + return + } + + // If not, suggest a connection to the best match. + result.addSuggested("satisfy global hop constraint", m.regardedPins[0]) + } +} diff --git a/spn/navigator/optimize_region.go b/spn/navigator/optimize_region.go new file mode 100644 index 00000000..14814813 --- /dev/null +++ b/spn/navigator/optimize_region.go @@ -0,0 +1,224 @@ +package navigator + +import ( + "fmt" + "sort" +) + +func (or *OptimizationResult) markSuggestedReachableInRegion(suggested *Pin, hopDistance int) { + // Abort if suggested Pin has no region. + if suggested.region == nil { + return + } + + // Don't update if distance is greater or equal than current one. + if hopDistance >= suggested.analysis.SuggestedHopDistanceInRegion { + return + } + + // Set suggested hop distance. + suggested.analysis.SuggestedHopDistanceInRegion = hopDistance + + // Increase distance and apply to matching Pins. + hopDistance++ + for _, lane := range suggested.ConnectedTo { + if lane.Pin.region != nil && + lane.Pin.region.ID == suggested.region.ID && + or.matcher(lane.Pin) { + or.markSuggestedReachableInRegion(lane.Pin, hopDistance) + } + } +} + +func (m *Map) optimizeForLowestCostInRegion(result *OptimizationResult) { + if m.home == nil || m.home.region == nil { + return + } + region := m.home.region + + // Add approach. + result.addApproach(fmt.Sprintf("Connect to best (lowest cost) %d Hubs within the region.", region.internalMinLanesOnHub)) + + // Sort by lowest cost. + sort.Sort(sortByLowestMeasuredCost(region.regardedPins)) + + // Add to suggested pins. + if len(region.regardedPins) <= region.internalMinLanesOnHub { + result.addSuggested("best in region", region.regardedPins...) + } else { + result.addSuggested("best in region", region.regardedPins[:region.internalMinLanesOnHub]...) + } +} + +func (m *Map) optimizeForDistanceConstraintInRegion(result *OptimizationResult, max int) { + if m.home == nil || m.home.region == nil { + return + } + region := m.home.region + + // Add approach. + result.addApproach(fmt.Sprintf("Satisfy max hop constraint of %d within the region.", region.internalMaxHops)) + + // Sort by lowest cost. + sort.Sort(sortBySuggestedHopDistanceInRegionAndLowestMeasuredCost(region.regardedPins)) + + for i := 0; i < max && i < len(region.regardedPins); i++ { + // Return when all regarded Pins are within the distance constraint. + if region.regardedPins[i].analysis.SuggestedHopDistanceInRegion <= region.internalMaxHops { + return + } + + // If not, suggest a connection to the best match. + result.addSuggested("satisfy regional hop constraint", region.regardedPins[i]) + } +} + +func (m *Map) optimizeForRegionConnectivity(result *OptimizationResult) { + if m.home == nil || m.home.region == nil { + return + } + region := m.home.region + + // Add approach. + result.addApproach("Connect region to other regions.") + + // Optimize for every region. +checkRegions: + for _, otherRegion := range m.regions { + // Skip own region. + if region.ID == otherRegion.ID { + continue + } + + // Collect data on connections to that region. + lanesToRegion, highestCostWithinLaneLimit := m.countConnectionsToRegion(result, region, otherRegion) + + // Sort by lowest cost. + sort.Sort(sortByLowestMeasuredCost(otherRegion.regardedPins)) + + // Find cheapest connections with a free slot or better values. + var lanesSuggested int + for _, pin := range otherRegion.regardedPins { + myCost := pin.measurements.GetCalculatedCost() + + // Check if we are done or region is satisfied. + switch { + case lanesSuggested >= region.regionalMaxLanesOnHub: + // We hit our max. + continue checkRegions + case lanesToRegion >= otherRegion.regionalMinLanes && myCost >= highestCostWithinLaneLimit: + // Region has enough lanes and we are not better. + continue checkRegions + } + + // Check if we can contribute on this Pin. + switch { + case pin.analysis.CrossRegionalConnections < otherRegion.regionalMaxLanesOnHub && + lanesToRegion < otherRegion.regionalMinLanes: + // There is a free spot on this Pin and the region needs more connections. + result.addSuggested("occupy cross-region lane on pin", pin) + lanesSuggested++ + lanesToRegion++ + // Because our own Pin is not counted, this should be the default + // suggestion for a stable network. + + case myCost < pin.analysis.CrossRegionalHighestCostInHubLimit: + // We have a better connection to this Pin than at least one other existing connection (within the limit!). + result.addSuggested("replace cross-region lane on pin", pin) + lanesSuggested++ + lanesToRegion++ + + case myCost < highestCostWithinLaneLimit && + pin.analysis.CrossRegionalConnections < otherRegion.regionalMaxLanesOnHub: + // We have a better connection to this Pin than another existing region-to-region connection. + result.addSuggested("replace unrelated cross-region lane", pin) + lanesSuggested++ + lanesToRegion++ + } + } + } +} + +// countConnectionsToRegion analyzes existing lanes from this to another +// region, with taking lanes from this Hub into account. +func (m *Map) countConnectionsToRegion(result *OptimizationResult, region *Region, otherRegion *Region) (lanesToRegion int, highestCostWithinLaneLimit float32) { + for _, pin := range region.regardedPins { + // Skip self. + if m.home.Hub.ID == pin.Hub.ID { + continue + } + + // Find lanes to other region. + for _, lane := range pin.ConnectedTo { + if lane.Pin.region != nil && + lane.Pin.region.ID == otherRegion.ID && + result.matcher(lane.Pin) { + // This is a lane from this region to a regarded Pin in the other region. + lanesToRegion++ + + // Count cross region connection. + lane.Pin.analysis.CrossRegionalConnections++ + + // Collect lane costs. + lane.Pin.analysis.CrossRegionalLaneCosts = append( + lane.Pin.analysis.CrossRegionalLaneCosts, + lane.Cost, + ) + } + } + } + + // Calculate lane costs from collected lane costs. + for _, pin := range otherRegion.regardedPins { + sort.Sort(sortCostsByLowest(pin.analysis.CrossRegionalLaneCosts)) + switch { + case len(pin.analysis.CrossRegionalLaneCosts) == 0: + // Nothing to do. + case len(pin.analysis.CrossRegionalLaneCosts) < otherRegion.regionalMaxLanesOnHub: + pin.analysis.CrossRegionalLowestCostLane = pin.analysis.CrossRegionalLaneCosts[0] + pin.analysis.CrossRegionalHighestCostInHubLimit = pin.analysis.CrossRegionalLaneCosts[len(pin.analysis.CrossRegionalLaneCosts)-1] + default: + pin.analysis.CrossRegionalLowestCostLane = pin.analysis.CrossRegionalLaneCosts[0] + pin.analysis.CrossRegionalHighestCostInHubLimit = pin.analysis.CrossRegionalLaneCosts[otherRegion.regionalMaxLanesOnHub-1] + } + + // Find highest cost within limit. + if pin.analysis.CrossRegionalHighestCostInHubLimit > highestCostWithinLaneLimit { + highestCostWithinLaneLimit = pin.analysis.CrossRegionalHighestCostInHubLimit + } + } + + return lanesToRegion, highestCostWithinLaneLimit +} + +func (m *Map) optimizeForSatelliteConnectivity(result *OptimizationResult) { + if m.home == nil { + return + } + // This is only for Hubs that are not in a region. + if m.home.region != nil { + return + } + + // Add approach. + result.addApproach("Connect satellite to regions.") + + // Optimize for every region. + for _, region := range m.regions { + // Sort by lowest cost. + sort.Sort(sortByLowestMeasuredCost(region.regardedPins)) + + // Add to suggested pins. + if len(region.regardedPins) <= region.satelliteMinLanes { + result.addSuggested(fmt.Sprintf("best to region %s", region.ID), region.regardedPins...) + } else { + result.addSuggested(fmt.Sprintf("best to region %s", region.ID), region.regardedPins[:region.satelliteMinLanes]...) + } + } +} + +type sortCostsByLowest []float32 + +func (a sortCostsByLowest) Len() int { return len(a) } +func (a sortCostsByLowest) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a sortCostsByLowest) Less(i, j int) bool { return a[i] < a[j] } diff --git a/spn/navigator/optimize_test.go b/spn/navigator/optimize_test.go new file mode 100644 index 00000000..83f778cf --- /dev/null +++ b/spn/navigator/optimize_test.go @@ -0,0 +1,188 @@ +package navigator + +import ( + "strings" + "sync" + "testing" + + "github.com/safing/portmaster/spn/hub" +) + +var ( + optimizedDefaultMapCreate sync.Once + optimizedDefaultMap *Map +) + +func getOptimizedDefaultTestMap(t *testing.T) *Map { + t.Helper() + + optimizedDefaultMapCreate.Do(func() { + optimizedDefaultMap = createRandomTestMap(2, 100) + optimizedDefaultMap.optimizeTestMap(t) + }) + return optimizedDefaultMap +} + +func (m *Map) optimizeTestMap(t *testing.T) { + t.Helper() + t.Logf("optimizing test map %s with %d pins", m.Name, len(m.all)) + + // Save original Home, as we will be switching around the home for the + // optimization. + run := 0 + newLanes := 0 + originalHome := m.home + mcf := newMeasurementCachedFactory() + + for { + run++ + newLanesInRun := 0 + // Let's check if we have a run without any map changes. + lastRun := true + + for _, pin := range m.all { + // Set Home to this Pin for this iteration. + if !m.SetHome(pin.Hub.ID, nil) { + panic("failed to set home") + } + + // Update measurements for the new home. + updateMeasurements(m, mcf) + + optimizeResult, err := m.optimize(nil) + if err != nil { + panic(err) + } + lanesCreatedWithResult := 0 + for _, connectTo := range optimizeResult.SuggestedConnections { + // Check if lane to suggested Hub already exists. + if m.home.Hub.GetLaneTo(connectTo.Hub.ID) != nil { + continue + } + + // Add lanes to the Hub status. + _ = m.home.Hub.AddLane(createLane(connectTo.Hub.ID)) + _ = connectTo.Hub.AddLane(createLane(m.home.Hub.ID)) + + // Update Hubs in map. + m.UpdateHub(m.home.Hub) + m.UpdateHub(connectTo.Hub) + newLanes++ + newLanesInRun++ + + // We are changing the map in this run, so this is not the last. + lastRun = false + + // Only create as many lanes as suggested by the result. + lanesCreatedWithResult++ + if lanesCreatedWithResult >= optimizeResult.MaxConnect { + break + } + } + if optimizeResult.Purpose != OptimizePurposeTargetStructure { + // If we aren't yet building the target structure, we need to keep building. + lastRun = false + } + } + + // Log progress. + if t != nil { + t.Logf( + "optimizing: added %d lanes in run #%d (%d Hubs) - %d new lanes in total", + newLanesInRun, + run, + len(m.all), + newLanes, + ) + } + + // End optimization after last run. + if lastRun { + break + } + } + + // Log what was done and set home back to the original value. + if t != nil { + t.Logf("finished optimizing test map %s: added %d lanes in %d runs", m.Name, newLanes, run) + } + m.home = originalHome +} + +func TestOptimize(t *testing.T) { + t.Parallel() + + m := getOptimizedDefaultTestMap(t) + matcher := m.defaultOptions().Destination.Matcher(m.intel) + originalHome := m.home + + for _, pin := range m.all { + // Set Home to this Pin for this iteration. + m.home = pin + err := m.recalculateReachableHubs() + if err != nil { + panic(err) + } + + for _, peer := range m.all { + // Check if the Pin matches the criteria. + if !matcher(peer) { + continue + } + + // TODO: Adapt test to new regions. + if peer.HopDistance > 5 { + t.Errorf("Optimization error: %s is %d hops away from %s", peer, peer.HopDistance, pin) + } + } + } + + // Print stats + t.Logf("optimized map:\n%s\n", m.Stats()) + + m.home = originalHome +} + +func updateMeasurements(m *Map, mcf *measurementCachedFactory) { + for _, pin := range m.all { + pin.measurements = mcf.getOrCreate(m.home.Hub.ID, pin.Hub.ID) + } +} + +type measurementCachedFactory struct { + cache map[string]*hub.Measurements +} + +func newMeasurementCachedFactory() *measurementCachedFactory { + return &measurementCachedFactory{ + cache: make(map[string]*hub.Measurements), + } +} + +func (mcf *measurementCachedFactory) getOrCreate(from, to string) *hub.Measurements { + var id string + comparison := strings.Compare(from, to) + switch { + case comparison == 0: + return nil + case comparison > 0: + id = from + "-" + to + case comparison < 0: + id = to + "-" + from + } + + m, ok := mcf.cache[id] + if ok { + return m + } + + m = hub.NewMeasurements() + m.Latency = createLatency() + m.Capacity = createCapacity() + m.CalculatedCost = CalculateLaneCost( + m.Latency, + m.Capacity, + ) + mcf.cache[id] = m + return m +} diff --git a/spn/navigator/options.go b/spn/navigator/options.go new file mode 100644 index 00000000..05c93ea1 --- /dev/null +++ b/spn/navigator/options.go @@ -0,0 +1,330 @@ +package navigator + +import ( + "context" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/service/profile" + "github.com/safing/portmaster/service/profile/endpoints" + "github.com/safing/portmaster/spn/hub" +) + +// HubType is the usage type of a Hub in routing. +type HubType uint8 + +// Hub Types. +const ( + HomeHub HubType = iota + TransitHub + DestinationHub +) + +// DeriveTunnelOptions derives and returns the tunnel options from the connection and profile. +// This function lives in firewall/tunnel.go and is set here to avoid import loops. +var DeriveTunnelOptions func(lp *profile.LayeredProfile, destination *intel.Entity, connEncrypted bool) *Options + +// Options holds configuration options for operations with the Map. +type Options struct { //nolint:maligned + // Home holds the options for Home Hubs. + Home *HomeHubOptions + + // Transit holds the options for Transit Hubs. + Transit *TransitHubOptions + + // Destination holds the options for Destination Hubs. + Destination *DestinationHubOptions + + // RoutingProfile defines the algorithm to use to find a route. + RoutingProfile string +} + +// HomeHubOptions holds configuration options for Home Hub operations with the Map. +type HomeHubOptions HubOptions + +// TransitHubOptions holds configuration options for Transit Hub operations with the Map. +type TransitHubOptions HubOptions + +// DestinationHubOptions holds configuration options for Destination Hub operations with the Map. +type DestinationHubOptions HubOptions + +// HubOptions holds configuration options for a specific hub type for operations with the Map. +type HubOptions struct { + // Regard holds required States. Only Hubs where all of these are present + // will taken into account for the operation. If NoDefaults is not set, a + // basic set of desirable states is added automatically. + Regard PinState + + // Disregard holds disqualifying States. Only Hubs where none of these are + // present will be taken into account for the operation. If NoDefaults is not + // set, a basic set of undesirable states is added automatically. + Disregard PinState + + // NoDefaults declares whether default and recommended Regard and Disregard states should not be used. + NoDefaults bool + + // HubPolicies is a collection of endpoint lists that Hubs must pass in order + // to be taken into account for the operation. + HubPolicies []endpoints.Endpoints + + // RequireVerifiedOwners specifies which verified owners are allowed to be used. + // If the list is empty, all owners are allowed. + RequireVerifiedOwners []string + + // CheckHubPolicyWith provides an entity that must match the Hubs entry or exit + // policy (depending on type) in order to be taken into account for the operation. + CheckHubPolicyWith *intel.Entity +} + +// Copy returns a shallow copy of the Options. +func (o *Options) Copy() *Options { + copied := &Options{ + RoutingProfile: o.RoutingProfile, + } + if o.Home != nil { + c := HomeHubOptions(HubOptions(*o.Home).Copy()) + copied.Home = &c + } + if o.Transit != nil { + c := TransitHubOptions(HubOptions(*o.Transit).Copy()) + copied.Transit = &c + } + if o.Destination != nil { + c := DestinationHubOptions(HubOptions(*o.Destination).Copy()) + copied.Destination = &c + } + return copied +} + +// Copy returns a shallow copy of the Options. +func (o HubOptions) Copy() HubOptions { + return HubOptions{ + Regard: o.Regard, + Disregard: o.Disregard, + NoDefaults: o.NoDefaults, + HubPolicies: o.HubPolicies, + RequireVerifiedOwners: o.RequireVerifiedOwners, + CheckHubPolicyWith: o.CheckHubPolicyWith, + } +} + +// PinMatcher is a stateful matching function generated by Options. +type PinMatcher func(pin *Pin) bool + +// DefaultOptions returns the default options for this Map. +func (m *Map) DefaultOptions() *Options { + m.Lock() + defer m.Unlock() + + return m.defaultOptions() +} + +func (m *Map) defaultOptions() *Options { + opts := &Options{ + RoutingProfile: DefaultRoutingProfileID, + } + + return opts +} + +// HubPoliciesAreSet returns whether any of the given hub policies are set and non-empty. +func HubPoliciesAreSet(policies []endpoints.Endpoints) bool { + for _, policy := range policies { + if policy.IsSet() { + return true + } + } + return false +} + +var emptyHubOptions = &HubOptions{} + +// Matcher generates a PinMatcher based on the Options. +func (o *HomeHubOptions) Matcher(hubIntel *hub.Intel) PinMatcher { + if o == nil { + return emptyHubOptions.Matcher(HomeHub, hubIntel) + } + + // Convert and call base func. + ho := HubOptions(*o) + return ho.Matcher(HomeHub, hubIntel) +} + +// Matcher generates a PinMatcher based on the Options. +func (o *TransitHubOptions) Matcher(hubIntel *hub.Intel) PinMatcher { + if o == nil { + return emptyHubOptions.Matcher(TransitHub, hubIntel) + } + + // Convert and call base func. + ho := HubOptions(*o) + return ho.Matcher(TransitHub, hubIntel) +} + +// Matcher generates a PinMatcher based on the Options. +func (o *DestinationHubOptions) Matcher(hubIntel *hub.Intel) PinMatcher { + if o == nil { + return emptyHubOptions.Matcher(DestinationHub, hubIntel) + } + + // Convert and call base func. + ho := HubOptions(*o) + return ho.Matcher(DestinationHub, hubIntel) +} + +// Matcher generates a PinMatcher based on the Options. +// Always use the Matcher on option structs if you can. +func (o *Options) Matcher(hubType HubType, hubIntel *hub.Intel) PinMatcher { + switch hubType { + case HomeHub: + return o.Home.Matcher(hubIntel) + case TransitHub: + return o.Transit.Matcher(hubIntel) + case DestinationHub: + return o.Destination.Matcher(hubIntel) + default: + return nil // This will panic, but should never be used. + } +} + +// Matcher generates a PinMatcher based on the Options. +func (o *HubOptions) Matcher(hubType HubType, hubIntel *hub.Intel) PinMatcher { + // Fallback to empty hub options. + if o == nil { + o = emptyHubOptions + } + + // Compile states to regard and disregard. + regard := o.Regard + disregard := o.Disregard + + // Add default states. + if !o.NoDefaults { + // Add default States. + regard = regard.Add(StateSummaryRegard) + disregard = disregard.Add(StateSummaryDisregard) + + // Add type based Advisories. + switch hubType { + case HomeHub: + // Home Hubs don't need to be reachable and don't need keys ready to be used. + regard = regard.Remove(StateReachable) + regard = regard.Remove(StateActive) + // Follow advisory. + disregard = disregard.Add(StateUsageAsHomeDiscouraged) + // Home Hub may be the current Home Hub. + disregard = disregard.Remove(StateIsHomeHub) + case TransitHub: + // Transit Hubs get no additional states. + case DestinationHub: + // Follow advisory. + disregard = disregard.Add(StateUsageAsDestinationDiscouraged) + // Do not use if Hub reports network issues. + disregard = disregard.Add(StateConnectivityIssues) + } + } + + // Add intel policies. + hubPolicies := o.HubPolicies + if hubIntel != nil && hubIntel.Parsed() != nil { + switch hubType { + case HomeHub: + hubPolicies = append(hubPolicies, hubIntel.Parsed().HubAdvisory, hubIntel.Parsed().HomeHubAdvisory) + case TransitHub: + hubPolicies = append(hubPolicies, hubIntel.Parsed().HubAdvisory) + case DestinationHub: + hubPolicies = append(hubPolicies, hubIntel.Parsed().HubAdvisory, hubIntel.Parsed().DestinationHubAdvisory) + } + } + + // Add entry/exit policiy checks. + checkHubPolicyWith := o.CheckHubPolicyWith + + return func(pin *Pin) bool { + // Check required Pin States. + if !pin.State.Has(regard) || pin.State.HasAnyOf(disregard) { + return false + } + + // Check verified owners. + if len(o.RequireVerifiedOwners) > 0 { + // Check if Pin has a verified owner at all. + if pin.VerifiedOwner == "" { + return false + } + + // Check if verified owner is in the list. + inList := false + for _, allowed := range o.RequireVerifiedOwners { + if pin.VerifiedOwner == allowed { + inList = true + break + } + } + + // Pin does not have a verified owner from the allowed list. + if !inList { + return false + } + } + + // Check policies. + policyCheck: + for _, policy := range hubPolicies { + // Check if policy is set. + if !policy.IsSet() { + continue + } + + // Check if policy matches. + result, reason := policy.MatchMulti(context.TODO(), pin.EntityV4, pin.EntityV6) + switch result { + case endpoints.NoMatch: + // Continue with check. + case endpoints.MatchError: + log.Warningf("spn/navigator: failed to match policy: %s", reason) + // Continue with check for now. + // TODO: Rethink how to do this. If eg. the geoip database has a + // problem, then no Hub will match. For now, just continue to the + // next rule set. Not optimal, but fail safe. + case endpoints.Denied: + // Explicitly denied, abort immediately. + return false + case endpoints.Permitted: + // Explicitly allowed, abort check and continue. + break policyCheck + } + } + + // Check entry/exit policies. + if checkHubPolicyWith != nil { + switch hubType { + case HomeHub: + if endpointListMatch(pin.Hub.Info.EntryPolicy(), checkHubPolicyWith) == endpoints.Denied { + // Hub does not allow entry from the given entity. + return false + } + case TransitHub: + // Transit Hubs do not have a hub policy. + case DestinationHub: + if endpointListMatch(pin.Hub.Info.ExitPolicy(), checkHubPolicyWith) == endpoints.Denied { + // Hub does not allow exit to the given entity. + return false + } + } + } + + return true // All checks have passed. + } +} + +func endpointListMatch(list endpoints.Endpoints, entity *intel.Entity) endpoints.EPResult { + // Check if endpoint list and entity are available. + if !list.IsSet() || entity == nil { + return endpoints.NoMatch + } + + // Match and return result only. + result, _ := list.Match(context.TODO(), entity) + return result +} diff --git a/spn/navigator/pin.go b/spn/navigator/pin.go new file mode 100644 index 00000000..9e113ab4 --- /dev/null +++ b/spn/navigator/pin.go @@ -0,0 +1,269 @@ +package navigator + +import ( + "context" + "net" + "strings" + "time" + + "github.com/tevino/abool" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/service/intel/geoip" + "github.com/safing/portmaster/spn/docks" + "github.com/safing/portmaster/spn/hub" +) + +// Pin represents a Hub on a Map. +type Pin struct { //nolint:maligned + // Hub Information + Hub *hub.Hub + EntityV4 *intel.Entity + EntityV6 *intel.Entity + LocationV4 *geoip.Location + LocationV6 *geoip.Location + + // Hub Status + State PinState + // VerifiedOwner holds the name of the verified owner / operator of the Hub. + VerifiedOwner string + // HopDistance signifies the needed hops to reach this Hub. + // HopDistance is measured from the view of a client. + // A Hub itself will have itself at distance 1. + // Directly connected Hubs have a distance of 2. + HopDistance int + // Cost is the routing cost of this Hub. + Cost float32 + // ConnectedTo holds validated lanes. + ConnectedTo map[string]*Lane // Key is Hub ID. + + // FailingUntil specifies until when this Hub should be regarded as failing. + // This is connected to StateFailing. + FailingUntil time.Time + + // Connection holds a information about a connection to the Hub of this Pin. + Connection *PinConnection + + // Internal + + // pushChanges is set to true if something noteworthy on the Pin changed and + // an update needs to be pushed by the database storage interface to whoever + // is listening. + pushChanges *abool.AtomicBool + + // measurements holds Measurements regarding this Pin. + // It must always be set and the reference must not be changed when measuring + // is enabled. + // Access to fields within are coordinated by itself. + measurements *hub.Measurements + + // analysis holds the analysis state. + // Should only be set during analysis and be reset at the start and removed at the end of an analysis. + analysis *AnalysisState + + // region is the region this Pin belongs to. + region *Region +} + +// PinConnection represents a connection to a terminal on the Hub. +type PinConnection struct { + // Terminal holds the active terminal session. + Terminal *docks.ExpansionTerminal + + // Route is the route built for this terminal. + Route *Route +} + +// Lane is a connection to another Hub. +type Lane struct { + // Pin is the Pin/Hub this Lane connects to. + Pin *Pin + + // Capacity designates the available bandwidth between these Hubs. + // It is specified in bit/s. + Capacity int + + // Lateny designates the latency between these Hubs. + // It is specified in nanoseconds. + Latency time.Duration + + // Cost is the routing cost of this lane. + Cost float32 + + // active is a helper flag in order help remove abandoned Lanes. + active bool +} + +// Lock locks the Pin via the Hub's lock. +func (pin *Pin) Lock() { + pin.Hub.Lock() +} + +// Unlock unlocks the Pin via the Hub's lock. +func (pin *Pin) Unlock() { + pin.Hub.Unlock() +} + +// String returns a human-readable representation of the Pin. +func (pin *Pin) String() string { + return "" +} + +// GetState returns the state of the pin. +func (pin *Pin) GetState() PinState { + pin.Lock() + defer pin.Unlock() + + return pin.State +} + +// updateLocationData fetches the necessary location data in order to correctly map out the Pin. +func (pin *Pin) updateLocationData() { + // TODO: We are currently assigning the Hub ID to the entity domain to + // support matching a Hub by its ID. The issue here is that the domain + // rules are lower-cased, so we have to lower-case the ID here too. + // This is not optimal from a security perspective, but there are still + // enough bits left that this cannot be easily exploited. + + if pin.Hub.Info.IPv4 != nil { + pin.EntityV4 = (&intel.Entity{ + IP: pin.Hub.Info.IPv4, + Domain: strings.ToLower(pin.Hub.ID) + ".", + }).Init(0) + + var ok bool + pin.LocationV4, ok = pin.EntityV4.GetLocation(context.TODO()) + if !ok { + log.Warningf("spn/navigator: failed to get location of %s of %s", pin.Hub.Info.IPv4, pin.Hub.StringWithoutLocking()) + return + } + } else { + pin.EntityV4 = nil + pin.LocationV4 = nil + } + + if pin.Hub.Info.IPv6 != nil { + pin.EntityV6 = (&intel.Entity{ + IP: pin.Hub.Info.IPv6, + Domain: strings.ToLower(pin.Hub.ID) + ".", + }).Init(0) + + var ok bool + pin.LocationV6, ok = pin.EntityV6.GetLocation(context.TODO()) + if !ok { + log.Warningf("spn/navigator: failed to get location of %s of %s", pin.Hub.Info.IPv6, pin.Hub.StringWithoutLocking()) + return + } + } else { + pin.EntityV6 = nil + pin.LocationV6 = nil + } +} + +// GetLocation returns the geoip location of the Pin, preferring first the given IP, then IPv4. +func (pin *Pin) GetLocation(ip net.IP) *geoip.Location { + pin.Lock() + defer pin.Unlock() + + switch { + case ip != nil && ip.Equal(pin.Hub.Info.IPv4) && pin.LocationV4 != nil: + return pin.LocationV4 + case ip != nil && ip.Equal(pin.Hub.Info.IPv6) && pin.LocationV6 != nil: + return pin.LocationV6 + case pin.LocationV4 != nil: + return pin.LocationV4 + case pin.LocationV6 != nil: + return pin.LocationV6 + default: + return nil + } +} + +// SetActiveTerminal sets an active terminal for the pin. +func (pin *Pin) SetActiveTerminal(pc *PinConnection) { + pin.Lock() + defer pin.Unlock() + + pin.Connection = pc + if pin.Connection != nil && pin.Connection.Terminal != nil { + pin.Connection.Terminal.SetChangeNotifyFunc(pin.NotifyTerminalChange) + } + + pin.pushChanges.Set() +} + +// GetActiveTerminal returns the active terminal of the pin. +func (pin *Pin) GetActiveTerminal() *docks.ExpansionTerminal { + pin.Lock() + defer pin.Unlock() + + if !pin.hasActiveTerminal() { + return nil + } + return pin.Connection.Terminal +} + +// HasActiveTerminal returns whether the Pin has an active terminal. +func (pin *Pin) HasActiveTerminal() bool { + pin.Lock() + defer pin.Unlock() + + return pin.hasActiveTerminal() +} + +func (pin *Pin) hasActiveTerminal() bool { + return pin.Connection != nil && + pin.Connection.Terminal.Abandoning.IsNotSet() +} + +// NotifyTerminalChange notifies subscribers of the changed terminal. +func (pin *Pin) NotifyTerminalChange() { + pin.pushChanges.Set() + pin.pushChange() +} + +// IsFailing returns whether the pin should be treated as failing. +// The Pin is locked for this. +func (pin *Pin) IsFailing() bool { + pin.Lock() + defer pin.Unlock() + + return time.Now().Before(pin.FailingUntil) +} + +// MarkAsFailingFor marks the pin as failing. +// The Pin is locked for this. +// Changes are pushed. +func (pin *Pin) MarkAsFailingFor(duration time.Duration) { + pin.Lock() + defer pin.Unlock() + + until := time.Now().Add(duration) + // Only ever increase failing until, never reduce. + if until.After(pin.FailingUntil) { + pin.FailingUntil = until + } + + pin.addStates(StateFailing) + + pin.pushChanges.Set() + pin.pushChange() +} + +// ResetFailingState resets the failing state. +// The Pin is locked for this. +// Changes are not pushed, but Pins are marked. +func (pin *Pin) ResetFailingState() { + pin.Lock() + defer pin.Unlock() + + if time.Now().Before(pin.FailingUntil) { + pin.FailingUntil = time.Now() + pin.pushChanges.Set() + } + if pin.State.Has(StateFailing) { + pin.removeStates(StateFailing) + pin.pushChanges.Set() + } +} diff --git a/spn/navigator/pin_export.go b/spn/navigator/pin_export.go new file mode 100644 index 00000000..85fd279e --- /dev/null +++ b/spn/navigator/pin_export.go @@ -0,0 +1,98 @@ +package navigator + +import ( + "sync" + "time" + + "github.com/safing/portbase/database/record" + "github.com/safing/portmaster/service/intel" + "github.com/safing/portmaster/spn/hub" +) + +// PinExport is the exportable version of a Pin. +type PinExport struct { + record.Base + sync.Mutex + + ID string + Name string + Map string + FirstSeen time.Time + + EntityV4 *intel.Entity + EntityV6 *intel.Entity + // TODO: add coords + + States []string // From pin.State + VerifiedOwner string + HopDistance int + + ConnectedTo map[string]*LaneExport // Key is Hub ID. + Route []string // Includes Home Hub and this Pin's ID. + SessionActive bool + + Info *hub.Announcement + Status *hub.Status +} + +// LaneExport is the exportable version of a Lane. +type LaneExport struct { + HubID string + + // Capacity designates the available bandwidth between these Hubs. + // It is specified in bit/s. + Capacity int + + // Lateny designates the latency between these Hubs. + // It is specified in nanoseconds. + Latency time.Duration +} + +// Export puts the Pin's information into an exportable format. +func (pin *Pin) Export() *PinExport { + pin.Lock() + defer pin.Unlock() + + // Shallow copy static values. + export := &PinExport{ + ID: pin.Hub.ID, + Name: pin.Hub.Info.Name, + Map: pin.Hub.Map, + FirstSeen: pin.Hub.FirstSeen, + EntityV4: pin.EntityV4, + EntityV6: pin.EntityV6, + States: pin.State.Export(), + VerifiedOwner: pin.VerifiedOwner, + HopDistance: pin.HopDistance, + SessionActive: pin.hasActiveTerminal() || pin.State.Has(StateIsHomeHub), + Info: pin.Hub.Info, // Is updated as a whole, no need to copy. + Status: pin.Hub.Status, // Is updated as a whole, no need to copy. + } + + // Export lanes. + export.ConnectedTo = make(map[string]*LaneExport, len(pin.ConnectedTo)) + for key, lane := range pin.ConnectedTo { + export.ConnectedTo[key] = &LaneExport{ + HubID: lane.Pin.Hub.ID, + Capacity: lane.Capacity, + Latency: lane.Latency, + } + } + + // Export route to Pin, if connected. + if pin.Connection != nil && pin.Connection.Route != nil { + export.Route = make([]string, len(pin.Connection.Route.Path)) + for key, hop := range pin.Connection.Route.Path { + export.Route[key] = hop.HubID + } + } + + // Create database record metadata. + export.SetKey(makeDBKey(export.Map, export.ID)) + export.SetMeta(&record.Meta{ + Created: export.FirstSeen.Unix(), + Modified: time.Now().Unix(), + }) + + return export +} diff --git a/spn/navigator/region.go b/spn/navigator/region.go new file mode 100644 index 00000000..a3798efe --- /dev/null +++ b/spn/navigator/region.go @@ -0,0 +1,231 @@ +package navigator + +import ( + "context" + "math" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/service/profile/endpoints" + "github.com/safing/portmaster/spn/hub" +) + +const ( + defaultRegionalMinLanesPerHub = 0.5 + defaultRegionalMaxLanesOnHub = 2 + defaultSatelliteMinLanesPerHub = 0.3 + defaultInternalMinLanesOnHub = 3 + defaultInternalMaxHops = 3 +) + +// Region specifies a group of Hubs for optimization purposes. +type Region struct { + ID string + Name string + config *hub.RegionConfig + memberPolicy endpoints.Endpoints + + pins []*Pin + regardedPins []*Pin + + regionalMinLanes int + regionalMaxLanesOnHub int + satelliteMinLanes int + internalMinLanesOnHub int + internalMaxHops int +} + +func (region *Region) getName() string { + switch { + case region == nil: + return "-" + case region.Name != "": + return region.Name + default: + return region.ID + } +} + +func (m *Map) updateRegions(config []*hub.RegionConfig) { + // Reset map and pins. + m.regions = make([]*Region, 0, len(config)) + for _, pin := range m.all { + pin.region = nil + } + + // Stop if not regions are defined. + if len(config) == 0 { + return + } + + // Build regions from config. + for _, regionConfig := range config { + // Check if region has an ID. + if regionConfig.ID == "" { + log.Error("spn/navigator: region is missing ID") + // Abort adding this region to the map. + continue + } + + // Create new region. + region := &Region{ + ID: regionConfig.ID, + Name: regionConfig.Name, + config: regionConfig, + } + + // Parse member policy. + if len(regionConfig.MemberPolicy) == 0 { + log.Errorf("spn/navigator: member policy of region %s is missing", region.ID) + // Abort adding this region to the map. + continue + } + memberPolicy, err := endpoints.ParseEndpoints(regionConfig.MemberPolicy) + if err != nil { + log.Errorf("spn/navigator: failed to parse member policy of region %s: %s", region.ID, err) + // Abort adding this region to the map. + continue + } + region.memberPolicy = memberPolicy + + // Recalculate region properties. + region.recalculateProperties() + + // Add region to map. + m.regions = append(m.regions, region) + } + + // Update region in all Pins. + for _, pin := range m.all { + m.updatePinRegion(pin) + } +} + +func (region *Region) addPin(pin *Pin) { + // Find pin in region. + for _, regionPin := range region.pins { + if pin.Hub.ID == regionPin.Hub.ID { + // Pin is already part of region. + return + } + } + + // Check if pin is already part of this region. + if pin.region != nil && pin.region.ID == region.ID { + return + } + + // Remove pin from previous region. + if pin.region != nil { + pin.region.removePin(pin) + } + + // Add new pin to region. + region.pins = append(region.pins, pin) + pin.region = region + + // Recalculate region properties. + region.recalculateProperties() +} + +func (region *Region) removePin(pin *Pin) { + // Find pin index in region. + removeIndex := -1 + for index, regionPin := range region.pins { + if pin.Hub.ID == regionPin.Hub.ID { + removeIndex = index + break + } + } + if removeIndex < 0 { + // Pin is not part of region. + return + } + + // Remove pin from region. + region.pins = append(region.pins[:removeIndex], region.pins[removeIndex+1:]...) + + // Recalculate region properties. + region.recalculateProperties() +} + +func (region *Region) recalculateProperties() { + // Regional properties. + region.regionalMinLanes = calculateMinLanes( + len(region.pins), + region.config.RegionalMinLanes, + region.config.RegionalMinLanesPerHub, + defaultRegionalMinLanesPerHub, + ) + region.regionalMaxLanesOnHub = region.config.RegionalMaxLanesOnHub + if region.regionalMaxLanesOnHub <= 0 { + region.regionalMaxLanesOnHub = defaultRegionalMaxLanesOnHub + } + + // Satellite properties. + region.satelliteMinLanes = calculateMinLanes( + len(region.pins), + region.config.SatelliteMinLanes, + region.config.SatelliteMinLanesPerHub, + defaultSatelliteMinLanesPerHub, + ) + + // Internal properties. + region.internalMinLanesOnHub = region.config.InternalMinLanesOnHub + if region.internalMinLanesOnHub <= 0 { + region.internalMinLanesOnHub = defaultInternalMinLanesOnHub + } + region.internalMaxHops = region.config.InternalMaxHops + if region.internalMaxHops <= 0 { + region.internalMaxHops = defaultInternalMaxHops + } + // Values below 2 do not make any sense for max hops. + if region.internalMaxHops < 2 { + region.internalMaxHops = 2 + } +} + +func calculateMinLanes(regionHubCount, minLanes int, minLanesPerHub, defaultMinLanesPerHub float64) (minLaneCount int) { + // Validate hub count. + if regionHubCount <= 0 { + // Reset to safe value. + regionHubCount = 1 + } + + // Set to configured minimum lanes. + minLaneCount = minLanes + + // Raise to configured minimum lanes per Hub. + if minLanesPerHub != 0 { + minLanesFromSize := int(math.Ceil(float64(regionHubCount) * minLanesPerHub)) + if minLanesFromSize > minLaneCount { + minLaneCount = minLanesFromSize + } + } + + // Raise to default minimum lanes per Hub, if still 0. + if minLaneCount <= 0 { + minLaneCount = int(math.Ceil(float64(regionHubCount) * defaultMinLanesPerHub)) + } + + return minLaneCount +} + +func (m *Map) updatePinRegion(pin *Pin) { + for _, region := range m.regions { + // Check if pin matches the region's member policy. + if pin.EntityV4 != nil { + result, _ := region.memberPolicy.Match(context.TODO(), pin.EntityV4) + if result == endpoints.Permitted { + region.addPin(pin) + return + } + } + if pin.EntityV6 != nil { + result, _ := region.memberPolicy.Match(context.TODO(), pin.EntityV6) + if result == endpoints.Permitted { + region.addPin(pin) + return + } + } + } +} diff --git a/spn/navigator/route.go b/spn/navigator/route.go new file mode 100644 index 00000000..f1b98a38 --- /dev/null +++ b/spn/navigator/route.go @@ -0,0 +1,221 @@ +package navigator + +import ( + "fmt" + mrand "math/rand" + "sort" + "strings" + "time" +) + +// Routes holds a collection of Routes. +type Routes struct { + All []*Route + randomizeTopPercent float32 + maxCost float32 // automatic + maxRoutes int // manual setting +} + +// Len is the number of elements in the collection. +func (r *Routes) Len() int { + return len(r.All) +} + +// Less reports whether the element with index i should sort before the element +// with index j. +func (r *Routes) Less(i, j int) bool { + return r.All[i].TotalCost < r.All[j].TotalCost +} + +// Swap swaps the elements with indexes i and j. +func (r *Routes) Swap(i, j int) { + r.All[i], r.All[j] = r.All[j], r.All[i] +} + +// isGoodEnough reports whether the route would survive a clean process. +func (r *Routes) isGoodEnough(route *Route) bool { + if r.maxCost > 0 && route.TotalCost > r.maxCost { + return false + } + return true +} + +// add adds a Route if it is good enough. +func (r *Routes) add(route *Route) { + if !r.isGoodEnough(route) { + return + } + r.All = append(r.All, route.CopyUpTo(0)) + r.clean() +} + +// clean sort and shortens the list to the configured maximum. +func (r *Routes) clean() { + // Sort Routes so that the best ones are on top. + sort.Sort(r) + // Remove all remaining from the list. + if len(r.All) > r.maxRoutes { + r.All = r.All[:r.maxRoutes] + } + // Set new maximum total cost. + if len(r.All) >= r.maxRoutes { + r.maxCost = r.All[len(r.All)-1].TotalCost + } +} + +// randomizeTop randomized to the top nearest pins for balancing the network. +func (r *Routes) randomizeTop() { + switch { + case r.randomizeTopPercent == 0: + // Check if randomization is enabled. + return + case len(r.All) < 2: + // Check if we have enough pins to work with. + return + } + + // Find randomization set. + randomizeUpTo := len(r.All) + threshold := r.All[0].TotalCost * (1 + r.randomizeTopPercent) + for i, r := range r.All { + // Find first value above the threshold to stop. + if r.TotalCost > threshold { + randomizeUpTo = i + break + } + } + + // Shuffle top set. + if randomizeUpTo >= 2 { + mr := mrand.New(mrand.NewSource(time.Now().UnixNano())) //nolint:gosec + mr.Shuffle(randomizeUpTo, r.Swap) + } +} + +// Route is a path through the map. +type Route struct { + // Path is a list of Transit Hubs and the Destination Hub, including the Cost + // for each Hop. + Path []*Hop + + // DstCost is the calculated cost between the Destination Hub and the destination IP. + DstCost float32 + + // TotalCost is the sum of all costs of this Route. + TotalCost float32 + + // Algorithm is the ID of the algorithm used to calculate the route. + Algorithm string +} + +// Hop is one hop of a route's path. +type Hop struct { + pin *Pin + + // HubID is the Hub ID. + HubID string + + // Cost is the cost for both Lane to this Hub and the Hub itself. + Cost float32 +} + +// addHop adds a hop to the route. +func (r *Route) addHop(pin *Pin, cost float32) { + r.Path = append(r.Path, &Hop{ + pin: pin, + Cost: cost, + }) + r.recalculateTotalCost() +} + +// completeRoute completes the route by adding the destination cost of the +// connection between the last hop and the destination IP. +func (r *Route) completeRoute(dstCost float32) { + r.DstCost = dstCost + r.recalculateTotalCost() +} + +// removeHop removes the last hop from the Route. +func (r *Route) removeHop() { + // Reset DstCost, as the route might have been completed. + r.DstCost = 0 + + if len(r.Path) >= 1 { + r.Path = r.Path[:len(r.Path)-1] + } + r.recalculateTotalCost() +} + +// recalculateTotalCost recalculates to total cost of this route. +func (r *Route) recalculateTotalCost() { + r.TotalCost = r.DstCost + for _, hop := range r.Path { + if hop.pin.HasActiveTerminal() { + // If we have an active connection, only take 80% of the cost. + r.TotalCost += hop.Cost * 0.8 + } else { + r.TotalCost += hop.Cost + } + } +} + +// CopyUpTo makes a somewhat deep copy of the Route up to the specified amount +// and returns it. Hops themselves are not copied, because their data does not +// change. Therefore, returned Hops may not be edited. +// Specify an amount of 0 to copy all. +func (r *Route) CopyUpTo(n int) *Route { + // Check amount. + if n == 0 || n > len(r.Path) { + n = len(r.Path) + } + + newRoute := &Route{ + Path: make([]*Hop, n), + DstCost: r.DstCost, + TotalCost: r.TotalCost, + } + copy(newRoute.Path, r.Path) + return newRoute +} + +// makeExportReady fills in all the missing data fields which are meant for +// exporting only. +func (r *Routes) makeExportReady(algorithm string) { + for _, route := range r.All { + route.makeExportReady(algorithm) + } +} + +// makeExportReady fills in all the missing data fields which are meant for +// exporting only. +func (r *Route) makeExportReady(algorithm string) { + r.Algorithm = algorithm + for _, hop := range r.Path { + hop.makeExportReady() + } +} + +// makeExportReady fills in all the missing data fields which are meant for +// exporting only. +func (hop *Hop) makeExportReady() { + hop.HubID = hop.pin.Hub.ID +} + +// Pin returns the Pin of the Hop. +func (hop *Hop) Pin() *Pin { + return hop.pin +} + +func (r *Route) String() string { + s := make([]string, 0, len(r.Path)+2) + s = append(s, fmt.Sprintf("route with %.2fc:", r.TotalCost)) + for i, hop := range r.Path { + if i == 0 { + s = append(s, hop.pin.String()) + } else { + s = append(s, fmt.Sprintf("--> %.2fc %s", hop.Cost, hop.pin)) + } + } + s = append(s, fmt.Sprintf("--> %.2fc", r.DstCost)) + return strings.Join(s, " ") +} diff --git a/spn/navigator/routing-profiles.go b/spn/navigator/routing-profiles.go new file mode 100644 index 00000000..9241c072 --- /dev/null +++ b/spn/navigator/routing-profiles.go @@ -0,0 +1,162 @@ +package navigator + +import ( + "github.com/safing/portbase/log" + "github.com/safing/portmaster/service/profile" +) + +// RoutingProfile defines a routing algorithm with some options. +type RoutingProfile struct { + ID string + + // Name is the human readable name of the profile. + Name string + + // MinHops defines how many hops a route must have at minimum. In order to + // reduce confusion, the Home Hub is also counted. + MinHops int + + // MaxHops defines the limit on how many hops a route may have. In order to + // reduce confusion, the Home Hub is also counted. + MaxHops int + + // MaxExtraHops sets a limit on how many extra hops are allowed in addition + // to the amount of Hops in the currently best route. This is an optimization + // option and should not interfere with finding the best route, but might + // reduce the amount of routes found. + MaxExtraHops int + + // MaxExtraCost sets a limit on the extra cost allowed in addition to the + // cost of the currently best route. This is an optimization option and + // should not interfere with finding the best route, but might reduce the + // amount of routes found. + MaxExtraCost float32 +} + +// Routing Profile Names. +const ( + RoutingProfileHomeID = "home" + RoutingProfileSingleHopID = "single-hop" + RoutingProfileDoubleHopID = "double-hop" + RoutingProfileTripleHopID = "triple-hop" +) + +// Routing Profiles. +var ( + DefaultRoutingProfileID = profile.DefaultRoutingProfileID + + RoutingProfileHome = &RoutingProfile{ + ID: "home", + Name: "Plain VPN Mode", + MinHops: 1, + MaxHops: 1, + } + RoutingProfileSingleHop = &RoutingProfile{ + ID: "single-hop", + Name: "Speed Focused", + MinHops: 1, + MaxHops: 3, + MaxExtraHops: 1, + MaxExtraCost: 10000, + } + RoutingProfileDoubleHop = &RoutingProfile{ + ID: "double-hop", + Name: "Balanced", + MinHops: 2, + MaxHops: 4, + MaxExtraHops: 2, + MaxExtraCost: 10000, + } + RoutingProfileTripleHop = &RoutingProfile{ + ID: "triple-hop", + Name: "Privacy Focused", + MinHops: 3, + MaxHops: 5, + MaxExtraHops: 3, + MaxExtraCost: 10000, + } +) + +// GetRoutingProfile returns the routing profile with the given ID. +func GetRoutingProfile(id string) *RoutingProfile { + switch id { + case RoutingProfileHomeID: + return RoutingProfileHome + case RoutingProfileSingleHopID: + return RoutingProfileSingleHop + case RoutingProfileDoubleHopID: + return RoutingProfileDoubleHop + case RoutingProfileTripleHopID: + return RoutingProfileTripleHop + default: + return RoutingProfileDoubleHop + } +} + +type routeCompliance uint8 + +const ( + routeOk routeCompliance = iota // Route is fully compliant and can be used. + routeNonCompliant // Route is not compliant, but this might change if more hops are added. + routeDisqualified // Route is disqualified and won't be able to become compliant. +) + +func (rp *RoutingProfile) checkRouteCompliance(route *Route, foundRoutes *Routes) routeCompliance { + switch { + case len(route.Path) < rp.MinHops: + // Route is shorter than the defined minimum. + return routeNonCompliant + case len(route.Path) > rp.MaxHops: + // Route is longer than the defined maximum. + return routeDisqualified + } + + // Check for hub re-use. + if len(route.Path) >= 2 { + lastHop := route.Path[len(route.Path)-1] + for _, hop := range route.Path[:len(route.Path)-1] { + if lastHop.pin.Hub.ID == hop.pin.Hub.ID { + return routeDisqualified + } + } + } + + // Check if hub is already in use, if so check if the route matches. + if len(route.Path) >= 2 { + // Get active connection to the last pin of the current path. + lastPinConnection := route.Path[len(route.Path)-1].pin.Connection + + switch { + case lastPinConnection == nil: + // Last pin is not yet connected. + case len(lastPinConnection.Route.Path) < 2: + // Path of last pin does not have enough hops. + // This is unexpected and should not happen. + log.Errorf( + "navigator: expected active connection to %s to have 2 hops or more on path, but it had %d", + route.Path[len(route.Path)-1].pin.Hub.StringWithoutLocking(), + len(lastPinConnection.Route.Path), + ) + case lastPinConnection.Route.Path[len(lastPinConnection.Route.Path)-2].pin.Hub.ID != route.Path[len(route.Path)-2].pin.Hub.ID: + // The previous hop of the existing route and the one we are evaluating don't match. + // Currently, we only allow one session per Hub. + return routeDisqualified + } + } + + // Abort route exploration when we are outside the optimization boundaries. + if len(foundRoutes.All) > 0 { + // Get the best found route. + best := foundRoutes.All[0] + // Abort if current route exceeds max extra costs. + if route.TotalCost > best.TotalCost+rp.MaxExtraCost { + return routeDisqualified + } + // Abort if current route exceeds max extra hops. + if len(route.Path) > len(best.Path)+rp.MaxExtraHops { + return routeDisqualified + } + } + + return routeOk +} diff --git a/spn/navigator/sort.go b/spn/navigator/sort.go new file mode 100644 index 00000000..9fd0391e --- /dev/null +++ b/spn/navigator/sort.go @@ -0,0 +1,141 @@ +package navigator + +type sortByPinID []*Pin + +func (a sortByPinID) Len() int { return len(a) } +func (a sortByPinID) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a sortByPinID) Less(i, j int) bool { return a[i].Hub.ID < a[j].Hub.ID } + +type sortByLowestMeasuredCost []*Pin + +func (a sortByLowestMeasuredCost) Len() int { return len(a) } +func (a sortByLowestMeasuredCost) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a sortByLowestMeasuredCost) Less(i, j int) bool { + x := a[i].measurements.GetCalculatedCost() + y := a[j].measurements.GetCalculatedCost() + if x != y { + return x < y + } + + // Fall back to geo proximity. + gx := a[i].measurements.GetGeoProximity() + gy := a[j].measurements.GetGeoProximity() + if gx != gy { + return gx > gy + } + + // Fall back to Hub ID. + return a[i].Hub.ID < a[j].Hub.ID +} + +type sortBySuggestedHopDistanceAndLowestMeasuredCost []*Pin + +func (a sortBySuggestedHopDistanceAndLowestMeasuredCost) Len() int { return len(a) } +func (a sortBySuggestedHopDistanceAndLowestMeasuredCost) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a sortBySuggestedHopDistanceAndLowestMeasuredCost) Less(i, j int) bool { + // First sort by suggested hop distance. + if a[i].analysis.SuggestedHopDistance != a[j].analysis.SuggestedHopDistance { + return a[i].analysis.SuggestedHopDistance > a[j].analysis.SuggestedHopDistance + } + + // Then by cost. + x := a[i].measurements.GetCalculatedCost() + y := a[j].measurements.GetCalculatedCost() + if x != y { + return x < y + } + + // Fall back to geo proximity. + gx := a[i].measurements.GetGeoProximity() + gy := a[j].measurements.GetGeoProximity() + if gx != gy { + return gx > gy + } + + // Fall back to Hub ID. + return a[i].Hub.ID < a[j].Hub.ID +} + +type sortBySuggestedHopDistanceInRegionAndLowestMeasuredCost []*Pin + +func (a sortBySuggestedHopDistanceInRegionAndLowestMeasuredCost) Len() int { return len(a) } +func (a sortBySuggestedHopDistanceInRegionAndLowestMeasuredCost) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a sortBySuggestedHopDistanceInRegionAndLowestMeasuredCost) Less(i, j int) bool { + // First sort by suggested hop distance. + if a[i].analysis.SuggestedHopDistanceInRegion != a[j].analysis.SuggestedHopDistanceInRegion { + return a[i].analysis.SuggestedHopDistanceInRegion > a[j].analysis.SuggestedHopDistanceInRegion + } + + // Then by cost. + x := a[i].measurements.GetCalculatedCost() + y := a[j].measurements.GetCalculatedCost() + if x != y { + return x < y + } + + // Fall back to geo proximity. + gx := a[i].measurements.GetGeoProximity() + gy := a[j].measurements.GetGeoProximity() + if gx != gy { + return gx > gy + } + + // Fall back to Hub ID. + return a[i].Hub.ID < a[j].Hub.ID +} + +type sortByLowestMeasuredLatency []*Pin + +func (a sortByLowestMeasuredLatency) Len() int { return len(a) } +func (a sortByLowestMeasuredLatency) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a sortByLowestMeasuredLatency) Less(i, j int) bool { + x, _ := a[i].measurements.GetLatency() + y, _ := a[j].measurements.GetLatency() + switch { + case x == y: + // Go to fallbacks. + case x == 0: + // Ignore zero values. + return false // j/y is better. + case y == 0: + // Ignore zero values. + return true // i/x is better. + default: + return x < y + } + + // Fall back to geo proximity. + gx := a[i].measurements.GetGeoProximity() + gy := a[j].measurements.GetGeoProximity() + if gx != gy { + return gx > gy + } + + // Fall back to Hub ID. + return a[i].Hub.ID < a[j].Hub.ID +} + +type sortByHighestMeasuredCapacity []*Pin + +func (a sortByHighestMeasuredCapacity) Len() int { return len(a) } +func (a sortByHighestMeasuredCapacity) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a sortByHighestMeasuredCapacity) Less(i, j int) bool { + x, _ := a[i].measurements.GetCapacity() + y, _ := a[j].measurements.GetCapacity() + if x != y { + return x > y + } + + // Fall back to geo proximity. + gx := a[i].measurements.GetGeoProximity() + gy := a[j].measurements.GetGeoProximity() + if gx != gy { + return gx > gy + } + + // Fall back to Hub ID. + return a[i].Hub.ID < a[j].Hub.ID +} diff --git a/spn/navigator/sort_test.go b/spn/navigator/sort_test.go new file mode 100644 index 00000000..f424cc3d --- /dev/null +++ b/spn/navigator/sort_test.go @@ -0,0 +1,112 @@ +package navigator + +import ( + "sort" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/safing/portmaster/spn/hub" +) + +func TestSorting(t *testing.T) { + t.Parallel() + + list := []*Pin{ + { + Hub: &hub.Hub{ + ID: "a", + }, + measurements: &hub.Measurements{ + Latency: 3, + Capacity: 4, + CalculatedCost: 5, + }, + analysis: &AnalysisState{ + SuggestedHopDistance: 3, + }, + }, + { + Hub: &hub.Hub{ + ID: "b", + }, + measurements: &hub.Measurements{ + Latency: 4, + Capacity: 3, + CalculatedCost: 1, + }, + analysis: &AnalysisState{ + SuggestedHopDistance: 2, + }, + }, + { + Hub: &hub.Hub{ + ID: "c", + }, + measurements: &hub.Measurements{ + Latency: 5, + Capacity: 2, + CalculatedCost: 2, + }, + analysis: &AnalysisState{ + SuggestedHopDistance: 4, + }, + }, + { + Hub: &hub.Hub{ + ID: "d", + }, + measurements: &hub.Measurements{ + Latency: 1, + Capacity: 1, + CalculatedCost: 3, + }, + analysis: &AnalysisState{ + SuggestedHopDistance: 4, + }, + }, + { + Hub: &hub.Hub{ + ID: "e", + }, + measurements: &hub.Measurements{ + Latency: 2, + Capacity: 5, + CalculatedCost: 4, + }, + analysis: &AnalysisState{ + SuggestedHopDistance: 4, + }, + }, + } + + sort.Sort(sortByLowestMeasuredCost(list)) + checkSorting(t, list, "b-c-d-e-a") + + sort.Sort(sortBySuggestedHopDistanceAndLowestMeasuredCost(list)) + checkSorting(t, list, "c-d-e-a-b") + + sort.Sort(sortByLowestMeasuredLatency(list)) + checkSorting(t, list, "d-e-a-b-c") + + sort.Sort(sortByHighestMeasuredCapacity(list)) + checkSorting(t, list, "e-a-b-c-d") + + sort.Sort(sortByPinID(list)) + checkSorting(t, list, "a-b-c-d-e") +} + +func checkSorting(t *testing.T, sortedList []*Pin, expectedOrder string) { + t.Helper() + + // Build list ID string. + ids := make([]string, 0, len(sortedList)) + for _, pin := range sortedList { + ids = append(ids, pin.Hub.ID) + } + sortedIDs := strings.Join(ids, "-") + + // Check for matching order. + assert.Equal(t, expectedOrder, sortedIDs, "should match") +} diff --git a/spn/navigator/state.go b/spn/navigator/state.go new file mode 100644 index 00000000..755e2895 --- /dev/null +++ b/spn/navigator/state.go @@ -0,0 +1,426 @@ +package navigator + +import ( + "strings" + "time" +) + +// PinState holds a bit-mapped collection of Pin states, or a single state used +// for assigment and matching. +type PinState uint16 + +const ( + // StateNone represents an empty state. + StateNone PinState = 0 + + // Negative States. + + // StateInvalid signifies that there was an error while processing or + // handling this Hub. + StateInvalid PinState = 1 << (iota - 1) // 1 << 0 => 00000001 => 0x01 + + // StateSuperseded signifies that this Hub was superseded by another. This is + // the case if any other Hub with a matching IP was verified after this one. + // Verification timestamp equals Hub.FirstSeen. + StateSuperseded // 0x02 + + // StateFailing signifies that a recent error was encountered while + // communicating with this Hub. Pin.FailingUntil specifies when this state is + // re-evaluated at earliest. + StateFailing // 0x04 + + // StateOffline signifies that the Hub is offline. + StateOffline // 0x08 + + // Positive States. + + // StateHasRequiredInfo signifies that the Hub announces the minimum required + // information about itself. + StateHasRequiredInfo // 0x10 + + // StateReachable signifies that the Hub is reachable via the network from + // the currently connected primary Hub. + StateReachable // 0x20 + + // StateActive signifies that everything seems fine with the Hub and + // connections to it should succeed. This is tested by checking if a valid + // semi-ephemeral public key is available. + StateActive // 0x40 + + _ // 0x80: Reserved + + // Trust and Advisory States. + + // StateTrusted signifies the Hub has the special trusted status. + StateTrusted // 0x0100 + + // StateUsageDiscouraged signifies that usage of the Hub is discouraged for any task. + StateUsageDiscouraged // 0x0200 + + // StateUsageAsHomeDiscouraged signifies that usage of the Hub as a Home Hub is discouraged. + StateUsageAsHomeDiscouraged // 0x0400 + + // StateUsageAsDestinationDiscouraged signifies that usage of the Hub as a Destination Hub is discouraged. + StateUsageAsDestinationDiscouraged // 0x0800 + + // Special States. + + // StateIsHomeHub signifies that the Hub is the current Home Hub. While not + // negative in itself, selecting the Home Hub does not make sense in almost + // all cases. + StateIsHomeHub // 0x1000 + + // StateConnectivityIssues signifies that the Hub reports connectivity issues. + // This might impact all connectivity or just some. + // This does not invalidate the Hub for all operations and not in all cases. + StateConnectivityIssues // 0x2000 + + // StateAllowUnencrypted signifies that the Hub is available to handle unencrypted connections. + StateAllowUnencrypted // 0x4000 + + // State Summaries. + + // StateSummaryRegard summarizes all states that must always be set in order to take a Hub into consideration for any task. + // TODO: Add StateHasRequiredInfo when we start enforcing Hub information. + StateSummaryRegard = StateReachable | StateActive + + // StateSummaryDisregard summarizes all states that must not be set in order to take a Hub into consideration for any task. + StateSummaryDisregard = StateInvalid | + StateSuperseded | + StateFailing | + StateOffline | + StateUsageDiscouraged | + StateIsHomeHub +) + +var allStates = []PinState{ + StateInvalid, + StateSuperseded, + StateFailing, + StateOffline, + StateHasRequiredInfo, + StateReachable, + StateActive, + StateTrusted, + StateUsageDiscouraged, + StateUsageAsHomeDiscouraged, + StateUsageAsDestinationDiscouraged, + StateIsHomeHub, + StateConnectivityIssues, + StateAllowUnencrypted, +} + +// Add returns a new PinState with the given states added. +func (pinState PinState) Add(states PinState) PinState { + // OR: + // 0011 + // | 0101 + // = 0111 + return pinState | states +} + +// Remove returns a new PinState with the given states removed. +func (pinState PinState) Remove(states PinState) PinState { + // AND NOT: + // 0011 + // &^ 0101 + // = 0010 + return pinState &^ states +} + +// Has returns whether the state has all of the given states. +func (pinState PinState) Has(states PinState) bool { + // AND: + // 0011 + // & 0101 + // = 0001 + + return pinState&states == states +} + +// HasAnyOf returns whether the state has any of the given states. +func (pinState PinState) HasAnyOf(states PinState) bool { + // AND: + // 0011 + // & 0101 + // = 0001 + + return (pinState & states) != 0 +} + +// HasNoneOf returns whether the state does not have any of the given states. +func (pinState PinState) HasNoneOf(states PinState) bool { + // AND: + // 0011 + // & 0101 + // = 0001 + + return (pinState & states) == 0 +} + +// addStates adds the given states on the Pin. +func (pin *Pin) addStates(states PinState) { + pin.State = pin.State.Add(states) +} + +// removeStates removes the given states on the Pin. +func (pin *Pin) removeStates(states PinState) { + pin.State = pin.State.Remove(states) +} + +func (m *Map) updateStateSuperseded(pin *Pin) { + pin.removeStates(StateSuperseded) + + // Update StateSuperseded + // Iterate over all Pins in order to find a matching IP address. + // In order to prevent false positive matching, we have to go through IPv4 + // and IPv6 separately. + // TODO: This will not scale well beyond about 1000 Hubs. + + // IPv4 Loop + if pin.Hub.Info.IPv4 != nil { + for _, mapPin := range m.all { + // Skip Pin itself + if mapPin.Hub.ID == pin.Hub.ID { + continue + } + + // Check for a matching IPv4 address. + if mapPin.Hub.Info.IPv4 != nil && pin.Hub.Info.IPv4.Equal(mapPin.Hub.Info.IPv4) { + continueChecking := checkAndHandleSuperseding(pin, mapPin) + if !continueChecking { + break + } + } + } + } + + // IPv6 Loop + if pin.Hub.Info.IPv6 != nil { + for _, mapPin := range m.all { + // Skip Pin itself + if mapPin.Hub.ID == pin.Hub.ID { + continue + } + + // Check for a matching IPv6 address. + if mapPin.Hub.Info.IPv6 != nil && pin.Hub.Info.IPv6.Equal(mapPin.Hub.Info.IPv6) { + continueChecking := checkAndHandleSuperseding(pin, mapPin) + if !continueChecking { + break + } + } + } + } +} + +func checkAndHandleSuperseding(newPin, existingPin *Pin) (continueChecking bool) { + const ( + supersedeNone = iota + supersedeExisting + supersedeNew + ) + var action int + + switch { + case newPin.Hub.ID == existingPin.Hub.ID: + // Cannot supersede same Hub. + // Continue checking. + action = supersedeNone + + // Step 1: Check if only one is active. + + case newPin.State.Has(StateActive) && existingPin.State.HasNoneOf(StateActive): + // If only the new Hub is active, supersede the existing one. + action = supersedeExisting + case newPin.State.HasNoneOf(StateActive) && existingPin.State.Has(StateActive): + // If only the existing Hub is active, supersede the new one. + action = supersedeNew + + // Step 2: Check if only one is reachable. + + case newPin.State.Has(StateReachable) && existingPin.State.HasNoneOf(StateReachable): + // If only the new Hub is reachable, supersede the existing one. + action = supersedeExisting + case newPin.State.HasNoneOf(StateReachable) && existingPin.State.Has(StateReachable): + // If only the existing Hub is reachable, supersede the new one. + action = supersedeNew + + // Step 3: Check which one has been seen first. + + case newPin.Hub.FirstSeen.After(existingPin.Hub.FirstSeen): + // If the new Hub has been first seen later, supersede the existing one. + action = supersedeExisting + default: + // If the existing Hub has been first seen later, supersede the new one. + action = supersedeNew + } + + switch action { + case supersedeExisting: + existingPin.addStates(StateSuperseded) + existingPin.pushChanges.Set() + // Continue checking, as there might be other Hubs to be superseded. + return true + + case supersedeNew: + newPin.addStates(StateSuperseded) + newPin.pushChanges.Set() + // If the new pin is superseded, do _not_ continue, as this will lead to an incorrect state. + return false + + case supersedeNone: + fallthrough + default: + // Do nothing, continue checking. + return true + } +} + +func (pin *Pin) updateStateHasRequiredInfo() { + pin.removeStates(StateHasRequiredInfo) + + // Check for required Hub Information. + switch { + case len(pin.Hub.Info.Name) == 0: + case len(pin.Hub.Info.Group) == 0: + case len(pin.Hub.Info.ContactAddress) == 0: + case len(pin.Hub.Info.ContactService) == 0: + case len(pin.Hub.Info.Hosters) == 0: + case len(pin.Hub.Info.Hosters[0]) == 0: + case len(pin.Hub.Info.Datacenter) == 0: + default: + pin.addStates(StateHasRequiredInfo) + } +} + +func (m *Map) updateActiveHubs() { + now := time.Now().Unix() + for _, pin := range m.all { + pin.updateStateActive(now) + } +} + +func (pin *Pin) updateStateActive(now int64) { + pin.removeStates(StateActive) + + // Check for active key. + for _, key := range pin.Hub.Status.Keys { + if now < key.Expires { + pin.addStates(StateActive) + return + } + } +} + +func (m *Map) recalculateReachableHubs() error { + if m.home == nil { + return ErrHomeHubUnset + } + + // reset + for _, pin := range m.all { + pin.removeStates(StateReachable) + pin.HopDistance = 0 + pin.pushChanges.Set() + } + + // find all connected Hubs + m.home.markReachable(1) + return nil +} + +func (pin *Pin) markReachable(hopDistance int) { + switch { + case !pin.State.Has(StateReachable): + // Pin wasn't reachable before. + case hopDistance < pin.HopDistance: + // New path has a shorter distance. + case pin.State.HasAnyOf(StateSummaryDisregard): //nolint:staticcheck + // Ignore disregarded pins for reachability calculation. + return + default: + // Pin is already reachable at same or better distance. + return + } + + // Update reachability. + pin.addStates(StateReachable) + pin.HopDistance = hopDistance + pin.pushChanges.Set() + + // Propagate to connected Pins. + hopDistance++ + for _, lane := range pin.ConnectedTo { + lane.Pin.markReachable(hopDistance) + } +} + +// Export returns a list of all state names. +func (pinState PinState) Export() []string { + // Check if there are no states. + if pinState == StateNone { + return nil + } + + // Collect state names. + var stateNames []string + for _, state := range allStates { + if pinState.Has(state) { + stateNames = append(stateNames, state.Name()) + } + } + + return stateNames +} + +// String returns the states as a human readable string. +func (pinState PinState) String() string { + stateNames := pinState.Export() + if len(stateNames) == 0 { + return "None" + } + + return strings.Join(stateNames, ", ") +} + +// Name returns the name of a single state flag. +func (pinState PinState) Name() string { + switch pinState { + case StateNone: + return "None" + case StateInvalid: + return "Invalid" + case StateSuperseded: + return "Superseded" + case StateFailing: + return "Failing" + case StateOffline: + return "Offline" + case StateHasRequiredInfo: + return "HasRequiredInfo" + case StateReachable: + return "Reachable" + case StateActive: + return "Active" + case StateTrusted: + return "Trusted" + case StateUsageDiscouraged: + return "UsageDiscouraged" + case StateUsageAsHomeDiscouraged: + return "UsageAsHomeDiscouraged" + case StateUsageAsDestinationDiscouraged: + return "UsageAsDestinationDiscouraged" + case StateIsHomeHub: + return "IsHomeHub" + case StateConnectivityIssues: + return "ConnectivityIssues" + case StateAllowUnencrypted: + return "AllowUnencrypted" + case StateSummaryRegard, StateSummaryDisregard: + // Satisfy exhaustive linter. + fallthrough + default: + return "Unknown" + } +} diff --git a/spn/navigator/state_test.go b/spn/navigator/state_test.go new file mode 100644 index 00000000..90d5f37a --- /dev/null +++ b/spn/navigator/state_test.go @@ -0,0 +1,31 @@ +package navigator + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStates(t *testing.T) { + t.Parallel() + + p := &Pin{} + + p.addStates(StateInvalid | StateFailing | StateSuperseded) + assert.Equal(t, StateInvalid|StateFailing|StateSuperseded, p.State) + + p.removeStates(StateFailing | StateSuperseded) + assert.Equal(t, StateInvalid, p.State) + + p.addStates(StateTrusted | StateActive) + assert.True(t, p.State.Has(StateInvalid|StateTrusted)) + assert.False(t, p.State.Has(StateInvalid|StateSuperseded)) + assert.True(t, p.State.HasAnyOf(StateInvalid|StateTrusted)) + assert.True(t, p.State.HasAnyOf(StateInvalid|StateSuperseded)) + assert.False(t, p.State.HasAnyOf(StateSuperseded|StateFailing)) + + assert.False(t, p.State.Has(StateSummaryRegard)) + assert.False(t, p.State.Has(StateSummaryDisregard)) + assert.True(t, p.State.HasAnyOf(StateSummaryRegard)) + assert.True(t, p.State.HasAnyOf(StateSummaryDisregard)) +} diff --git a/spn/navigator/testdata/main-intel.yml b/spn/navigator/testdata/main-intel.yml new file mode 100644 index 00000000..62711337 --- /dev/null +++ b/spn/navigator/testdata/main-intel.yml @@ -0,0 +1,234 @@ +--- +BootstrapHubs: +- tcp://[2a01:4f8:172:3753::2]:17#Zwtb8EKMatnMRkW1VaLh8CPV3QswD9iuRU4Sda8uLezUkC # fogos [DE] +- tcp://[2a01:4f9:2a:d48::2]:17#Zwkwujs345P4ZygNZcEafawTqfZieCBVogQZ3xZPWiu7BU # heleus [FI] +- tcp://138.201.140.70:17#Zwtb8EKMatnMRkW1VaLh8CPV3QswD9iuRU4Sda8uLezUkC # fogos [DE] +- tcp://95.216.13.61:17#Zwkwujs345P4ZygNZcEafawTqfZieCBVogQZ3xZPWiu7BU # heleus [FI] + +Hubs: + ZwhpYS1jWzXvPYKFhJqh1ZD3bKquLLoSoJ6RjeshmcXoFx: # voria [US] + Trusted: true + VerifiedOwner: Safing + Override: + CountryCode: US + Coordinates: # Ashburn, VA + Latitude: 39.04 + Longitude: -77.48 + AccuracyRadius: 20 + ZwkAKBoyEd3PkE5RGDNmghahzHiBiTZA7Mg3XH7X3HjS39: # noru [US] + Trusted: true + VerifiedOwner: Safing + ZwkapJz5HFWpgd9PHsZLVueBu9PDmTJHKp382Wm9MB2EB7: # lovas [US] + Trusted: true + VerifiedOwner: Safing + Override: + CountryCode: US + Coordinates: # Los Angeles, CA + Latitude: 34.03 + Longitude: -118.15 + AccuracyRadius: 20 + ZwkLShvVYvQFGmpY1MNhSSPXCktojywMVtv2N86mFbNH4w: # tooina [CA] + Trusted: true + VerifiedOwner: Safing + Zwkwujs345P4ZygNZcEafawTqfZieCBVogQZ3xZPWiu7BU: # heleus [FI] + Trusted: true + VerifiedOwner: Safing + Zwm72XieV6aeNKbwtJW8JdPUwT1hopQaLanLXjxcTfV3B9: # mergan [US] + Trusted: true + VerifiedOwner: Safing + Zwmp5SgUK9FidWBSCDK4d6dyRp3vhz3dQdwma1E4TMfiRw: # grenenia [FR] + Trusted: true + VerifiedOwner: Safing + Override: + CountryCode: FR + Coordinates: # Gravelines + Latitude: 50.59 + Longitude: 2.07 + AccuracyRadius: 20 + ZwnFd1bSQrBegPZqFkS7DZU29x4PbojpFmTQFUnzQoicKp: # telos [IL] + Trusted: true + VerifiedOwner: Safing + Zwpg5FoXYVYidzgbdvDyvBBcrArmmHvK9nH3v7KDHiywtt: # melcor [PL] + Trusted: true + VerifiedOwner: Safing + ZwpsJpwngWyba54AbVkCawcRQ2HP37RRQAgj5LHNR2svRf: # soalis [AU] + Trusted: true + VerifiedOwner: Safing + Zwpy5hbrQkKznJwbUmn9WpJwGkpWD9VqE2pi9yfMDQM7PK: # rin9 [FR] + Trusted: true + VerifiedOwner: Safing + Override: + CountryCode: FR + Coordinates: # Strasbourg + Latitude: 48.35 + Longitude: 7.45 + AccuracyRadius: 20 + ZwqANMrhcyJZb8cRMEd3FdPcXY7ZbvviPPfTUQpLNau12J: # sulkam [GB] + Trusted: true + VerifiedOwner: Safing + ZwwBspMhigqcEYv2cryipzJsi4vkHhnBqUmDmkJ2xizGFx: # surn [US] + Trusted: true + VerifiedOwner: Safing + Override: + CountryCode: US + Coordinates: # Seattle, WA + Latitude: 47.36 + Longitude: -122.19 + AccuracyRadius: 20 + ZwsvsES3SHz1VLnwFPxDbW6DC8Esp1PiEtUHxGnm4BTYHt: # fungvis [DE] + Trusted: true + VerifiedOwner: Safing + Zwtb8EKMatnMRkW1VaLh8CPV3QswD9iuRU4Sda8uLezUkC: # fogos [DS] + Trusted: true + VerifiedOwner: Safing + ZwtfvBuq5wkKYRth8rGCuGyp42nMe4doASUDJiDHJ8iucn: # vamalla [AT] + Trusted: true + VerifiedOwner: Safing + ZwtjwvdPxG4u7oB2zmNJFvsDy5VDLT9UArDkYDGfC9bkDt: # carros [US] + Trusted: true + VerifiedOwner: Safing + ZwvMZt6RcrrRuCdufjApnosxWbzsP8rTPRuHGeHu5KU241: # syniru [SG] + Trusted: true + VerifiedOwner: Safing + Override: + CountryCode: SG + Coordinates: # Singapore + Latitude: 1.18 + Longitude: 103.50 + AccuracyRadius: 20 + ZwvyDLz8221fcSBw6GKZNDnwEn4YmE9m7JPieLUVe7iGR9: # calla [CA] + Trusted: true + VerifiedOwner: Safing + Zwvz9S6uyxn4ww1JGqJiisGMDmH2hz6mhwutmJXvTtwQww: # cidai [US] + Trusted: true + VerifiedOwner: Safing + ZwxJvZDZH18RUEQ3oFcR5uCqeXJaqkoi9P5Sj1aZ62HPin: # nutis [DE] + Trusted: true + VerifiedOwner: Safing + ZwvPQVFkoDbx3J6qThNwfLHZqwvFgUYLYtirCHVd7FfjBz: # perturn [CZ] + Trusted: true + VerifiedOwner: Safing + Override: + CountryCode: CZ + Coordinates: # Prague + Latitude: 50.05 + Longitude: 14.25 + AccuracyRadius: 100 + Zwj52Q7d5ezvFk7HKB42dBtFu152bC9JasYF7BHB724RfG: # sono [NL] + Trusted: true + VerifiedOwner: Safing + ZwmhYMEmw36CzgVUp9sLjoK3gkVDWdMPiupEcekpTAXur8: # ivtos [TR] + Trusted: true + VerifiedOwner: Safing + Override: + CountryCode: TR + Coordinates: # Izmir + Latitude: 38.25 + Longitude: 27.90 + AccuracyRadius: 20 + Zwm9JX1hBNUUvSYc3gpMhmw84ay45SuyXE7D2UgETM7XCn: # porcania [PT] + Trusted: true + VerifiedOwner: Safing + ZwxE83uRV9LcM8Bm3QjXjjejNRhBBBJAethPf14R6gcZwf: # steepeus [SE] + Trusted: true + VerifiedOwner: Safing + +InfoOverrides: + workaround: + for: bug + +AdviseOnlyTrustedHubs: false +AdviseOnlyTrustedHomeHubs: true +AdviseOnlyTrustedDestinationHubs: false + +HomeHubAdvisory: +- "- Zwj52Q7d5ezvFk7HKB42dBtFu152bC9JasYF7BHB724RfG" # sono [NL] is too slow for home hub +- "- Zwm9JX1hBNUUvSYc3gpMhmw84ay45SuyXE7D2UgETM7XCn" # porcania [PT] is too slow for home hub +- "- ZwmhYMEmw36CzgVUp9sLjoK3gkVDWdMPiupEcekpTAXur8" # ivtos [TR] is too slow for home hub +- "- ZwvPQVFkoDbx3J6qThNwfLHZqwvFgUYLYtirCHVd7FfjBz" # perturn [CZ] is too slow for home hub + +Regions: +- ID: europe + Name: Europe + RegionalMinLanes: 5 + RegionalMinLanesPerHub: 0.7 + RegionalMaxLanesOnHub: 2 + SatelliteMinLanes: 2 + SatelliteMinLanesPerHub: 0.3 + InternalMinLanesOnHub: 3 + InternalMaxHops: 3 + MemberPolicy: + - "+ AD" + - "+ AL" + - "+ AT" + - "+ AX" + - "+ BA" + - "+ BE" + - "+ BG" + - "+ BY" + - "+ CH" + - "+ CZ" + - "+ DE" + - "+ DK" + - "+ EE" + - "+ ES" + - "+ FI" + - "+ FO" + - "+ FR" + - "+ GB" + - "+ GG" + - "+ GI" + - "+ GR" + - "+ HR" + - "+ HU" + - "+ IE" + - "+ IM" + - "+ IS" + - "+ IT" + - "+ JE" + - "+ LI" + - "+ LT" + - "+ LU" + - "+ LV" + - "+ MC" + - "+ MD" + - "+ ME" + - "+ MK" + - "+ MT" + - "+ NL" + - "+ NO" + - "+ PL" + - "+ PT" + - "+ RO" + - "+ RS" + - "+ RU" + - "+ SE" + - "+ SI" + - "+ SJ" + - "+ SK" + - "+ SM" + - "+ UA" + - "+ VA" +- ID: north-america + Name: "North America" + RegionalMinLanes: 5 + RegionalMinLanesPerHub: 0.7 + RegionalMaxLanesOnHub: 2 + SatelliteMinLanes: 2 + SatelliteMinLanesPerHub: 0.3 + InternalMinLanesOnHub: 3 + InternalMaxHops: 3 + MemberPolicy: + - "+ BM" + - "+ BZ" + - "+ CA" + - "+ CR" + - "+ GL" + - "+ GT" + - "+ HN" + - "+ MX" + - "+ NI" + - "+ PA" + - "+ PM" + - "+ SV" + - "+ US" \ No newline at end of file diff --git a/spn/navigator/update.go b/spn/navigator/update.go new file mode 100644 index 00000000..73f52811 --- /dev/null +++ b/spn/navigator/update.go @@ -0,0 +1,776 @@ +package navigator + +import ( + "context" + "fmt" + "path" + "strings" + "time" + + "github.com/tevino/abool" + "golang.org/x/exp/slices" + + "github.com/safing/portbase/config" + "github.com/safing/portbase/database" + "github.com/safing/portbase/database/query" + "github.com/safing/portbase/database/record" + "github.com/safing/portbase/log" + "github.com/safing/portbase/modules" + "github.com/safing/portbase/utils" + "github.com/safing/portmaster/service/intel/geoip" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/service/profile" + "github.com/safing/portmaster/spn/hub" +) + +var db = database.NewInterface(&database.Options{ + Local: true, + Internal: true, +}) + +// InitializeFromDatabase loads all Hubs from the given database prefix and adds them to the Map. +func (m *Map) InitializeFromDatabase() error { + m.Lock() + defer m.Unlock() + + // start query for Hubs + iter, err := db.Query(query.New(hub.MakeHubDBKey(m.Name, ""))) + if err != nil { + return fmt.Errorf("failed to start query for initialization feed of %s map: %w", m.Name, err) + } + + // update navigator + var hubCount int + log.Tracef("spn/navigator: starting to initialize %s map from database", m.Name) + for r := range iter.Next { + h, err := hub.EnsureHub(r) + if err != nil { + log.Warningf("spn/navigator: could not parse hub %q while initializing %s map: %s", r.Key(), m.Name, err) + continue + } + + hubCount++ + m.updateHub(h, false, true) + } + switch { + case iter.Err() != nil: + return fmt.Errorf("failed to (fully) initialize %s map: %w", m.Name, iter.Err()) + case hubCount == 0: + log.Warningf("spn/navigator: no hubs available for %s map - this is normal on first start", m.Name) + default: + log.Infof("spn/navigator: added %d hubs from database to %s map", hubCount, m.Name) + } + return nil +} + +// UpdateHook updates the a map from database changes. +type UpdateHook struct { + database.HookBase + m *Map +} + +// UsesPrePut implements the Hook interface. +func (hook *UpdateHook) UsesPrePut() bool { + return true +} + +// PrePut implements the Hook interface. +func (hook *UpdateHook) PrePut(r record.Record) (record.Record, error) { + // Remove deleted hubs from the map. + if r.Meta().IsDeleted() { + hook.m.RemoveHub(path.Base(r.Key())) + return r, nil + } + + // Ensure we have a hub and update it in navigation map. + h, err := hub.EnsureHub(r) + if err != nil { + log.Debugf("spn/navigator: record %s is not a hub", r.Key()) + } else { + hook.m.updateHub(h, true, false) + } + + return r, nil +} + +// RegisterHubUpdateHook registers a database pre-put hook that updates all +// Hubs saved at the given database prefix. +func (m *Map) RegisterHubUpdateHook() (err error) { + m.hubUpdateHook, err = database.RegisterHook( + query.New(hub.MakeHubDBKey(m.Name, "")), + &UpdateHook{m: m}, + ) + return err +} + +// CancelHubUpdateHook cancels the map's update hook. +func (m *Map) CancelHubUpdateHook() { + if m.hubUpdateHook != nil { + if err := m.hubUpdateHook.Cancel(); err != nil { + log.Warningf("spn/navigator: failed to cancel update hook for map %s: %s", m.Name, err) + } + } +} + +// RemoveHub removes a Hub from the Map. +func (m *Map) RemoveHub(id string) { + m.Lock() + defer m.Unlock() + + // Get pin and remove it from the map, if it exists. + pin, ok := m.all[id] + if !ok { + return + } + delete(m.all, id) + + // Remove lanes from removed Pin. + for id := range pin.ConnectedTo { + // Remove Lane from peer. + peer, ok := m.all[id] + if ok { + delete(peer.ConnectedTo, pin.Hub.ID) + peer.pushChanges.Set() + } + } + + // Push update to subscriptions. + export := pin.Export() + export.Meta().Delete() + mapDBController.PushUpdate(export) + // Push lane changes. + m.PushPinChanges() +} + +// UpdateHub updates a Hub on the Map. +func (m *Map) UpdateHub(h *hub.Hub) { + m.updateHub(h, true, true) +} + +func (m *Map) updateHub(h *hub.Hub, lockMap, lockHub bool) { + if lockMap { + m.Lock() + defer m.Unlock() + } + if lockHub { + h.Lock() + defer h.Unlock() + } + + // Hub requires both Info and Status to be added to the Map. + if h.Info == nil || h.Status == nil { + return + } + + // Create or update Pin. + pin, ok := m.all[h.ID] + if ok { + pin.Hub = h + } else { + pin = &Pin{ + Hub: h, + ConnectedTo: make(map[string]*Lane), + pushChanges: abool.New(), + } + m.all[h.ID] = pin + } + pin.pushChanges.Set() + + // 1. Update Pin Data. + + // Add/Update location data from IP addresses. + pin.updateLocationData() + + // Override Pin Data. + m.updateInfoOverrides(pin) + + // Update Hub cost. + pin.Cost = CalculateHubCost(pin.Hub.Status.Load) + + // Ensure measurements are set when enabled. + if m.measuringEnabled && pin.measurements == nil { + // Get shared measurements. + pin.measurements = pin.Hub.GetMeasurementsWithLockedHub() + + // Update cost calculation. + latency, _ := pin.measurements.GetLatency() + capacity, _ := pin.measurements.GetCapacity() + pin.measurements.SetCalculatedCost(CalculateLaneCost(latency, capacity)) + + // Update geo proximity. + // Get own location. + var myLocation *geoip.Location + switch { + case m.home != nil && m.home.LocationV4 != nil: + myLocation = m.home.LocationV4 + case m.home != nil && m.home.LocationV6 != nil: + myLocation = m.home.LocationV6 + default: + locations, ok := netenv.GetInternetLocation() + if ok { + myLocation = locations.Best().LocationOrNil() + } + } + // Calculate proximity with available location. + if myLocation != nil { + switch { + case pin.LocationV4 != nil: + pin.measurements.SetGeoProximity( + myLocation.EstimateNetworkProximity(pin.LocationV4), + ) + case pin.LocationV6 != nil: + pin.measurements.SetGeoProximity( + myLocation.EstimateNetworkProximity(pin.LocationV6), + ) + } + } + } + + // 2. Update Pin States. + + // Update the invalid status of the Pin. + if pin.Hub.InvalidInfo || pin.Hub.InvalidStatus { + pin.addStates(StateInvalid) + } else { + pin.removeStates(StateInvalid) + } + + // Update online status of the Pin. + if pin.Hub.HasFlag(hub.FlagOffline) || pin.Hub.Status.Version == hub.VersionOffline { + pin.addStates(StateOffline) + } else { + pin.removeStates(StateOffline) + } + + // Update online status of the Pin. + if pin.Hub.HasFlag(hub.FlagAllowUnencrypted) { + pin.addStates(StateAllowUnencrypted) + } else { + pin.removeStates(StateAllowUnencrypted) + } + + // Update from status flags. + if pin.Hub.HasFlag(hub.FlagNetError) { + pin.addStates(StateConnectivityIssues) + } else { + pin.removeStates(StateConnectivityIssues) + } + + // Update Trust and Advisory Statuses. + m.updateIntelStatuses(pin, cfgOptionTrustNodeNodes()) + + // Update Statuses derived from Hub. + pin.updateStateHasRequiredInfo() + pin.updateStateActive(time.Now().Unix()) + + // 3. Update Lanes. + + // Mark all existing Lanes as inactive. + for _, lane := range pin.ConnectedTo { + lane.active = false + } + + // Update Lanes (connections to other Hubs) from the Status. + for _, lane := range pin.Hub.Status.Lanes { + // Check if this is a Lane to itself. + if lane.ID == pin.Hub.ID { + continue + } + + // First, get the Lane peer. + peer, ok := m.all[lane.ID] + if !ok { + // We need to wait for peer to be added to the Map. + continue + } + + m.updateHubLane(pin, lane, peer) + } + + // Remove all inactive/abandoned Lanes from both Pins. + var removedLanes bool + for id, lane := range pin.ConnectedTo { + if !lane.active { + // Remove Lane from this Pin. + delete(pin.ConnectedTo, id) + pin.pushChanges.Set() + removedLanes = true + // Remove Lane from peer. + peer, ok := m.all[id] + if ok { + delete(peer.ConnectedTo, pin.Hub.ID) + peer.pushChanges.Set() + } + } + } + + // Fully recalculate reachability if any Lanes were removed. + if removedLanes { + err := m.recalculateReachableHubs() + if err != nil { + log.Warningf("spn/navigator: failed to recalculate reachable Hubs: %s", err) + } + } + + // 4. Update states that depend on other information. + + // Check if hub is superseded or if it supersedes another hub. + m.updateStateSuperseded(pin) + + // Push updates. + m.PushPinChanges() +} + +const ( + minUnconfirmedLatency = 10 * time.Millisecond + maxUnconfirmedCapacity = 100000000 // 100Mbit/s + + cap1Mbit float32 = 1000000 + cap10Mbit float32 = 10000000 + cap100Mbit float32 = 100000000 + cap1Gbit float32 = 1000000000 + cap10Gbit float32 = 10000000000 +) + +// updateHubLane updates a lane between two Hubs on the Map. +// pin must already be locked, lane belongs to pin. +// peer will be locked by this function. +func (m *Map) updateHubLane(pin *Pin, lane *hub.Lane, peer *Pin) { + peer.Hub.Lock() + defer peer.Hub.Unlock() + + // Then get the corresponding Lane from that peer, if it exists. + var peerLane *hub.Lane + for _, possiblePeerLane := range peer.Hub.Status.Lanes { + if possiblePeerLane.ID == pin.Hub.ID { + peerLane = possiblePeerLane + // We have found the corresponding peerLane, break the loop. + break + } + } + if peerLane == nil { + // The peer obviously does not advertise a Lane to this Hub. + // Maybe this is a fresh Lane, and the message has not yet reached us. + // Alternatively, the Lane could have been recently removed. + + // Abandon this Lane for now. + delete(pin.ConnectedTo, peer.Hub.ID) + return + } + + // Calculate combined latency, use the greater value. + combinedLatency := lane.Latency + if peerLane.Latency > combinedLatency { + combinedLatency = peerLane.Latency + } + // Enforce minimum value if at least one side has no data. + if (lane.Latency == 0 || peerLane.Latency == 0) && combinedLatency < minUnconfirmedLatency { + combinedLatency = minUnconfirmedLatency + } + + // Calculate combined capacity, use the lesser existing value. + combinedCapacity := lane.Capacity + if combinedCapacity == 0 || (peerLane.Capacity > 0 && peerLane.Capacity < combinedCapacity) { + combinedCapacity = peerLane.Capacity + } + // Enforce maximum value if at least one side has no data. + if (lane.Capacity == 0 || peerLane.Capacity == 0) && combinedCapacity > maxUnconfirmedCapacity { + combinedCapacity = maxUnconfirmedCapacity + } + + // Calculate lane cost. + laneCost := CalculateLaneCost(combinedLatency, combinedCapacity) + + // Add Lane to both Pins and override old values in the process. + pin.ConnectedTo[peer.Hub.ID] = &Lane{ + Pin: peer, + Capacity: combinedCapacity, + Latency: combinedLatency, + Cost: laneCost, + active: true, + } + peer.ConnectedTo[pin.Hub.ID] = &Lane{ + Pin: pin, + Capacity: combinedCapacity, + Latency: combinedLatency, + Cost: laneCost, + active: true, + } + peer.pushChanges.Set() + + // Check for reachability. + + if pin.State.Has(StateReachable) { + peer.markReachable(pin.HopDistance + 1) + } + if peer.State.Has(StateReachable) { + pin.markReachable(peer.HopDistance + 1) + } +} + +// ResetFailingStates resets the failing state on all pins. +func (m *Map) ResetFailingStates(ctx context.Context) { + m.Lock() + defer m.Unlock() + + for _, pin := range m.all { + pin.ResetFailingState() + } + + m.PushPinChanges() +} + +func (m *Map) updateFailingStates(ctx context.Context, task *modules.Task) error { + m.Lock() + defer m.Unlock() + + for _, pin := range m.all { + if pin.State.Has(StateFailing) && !pin.IsFailing() { + pin.removeStates(StateFailing) + } + } + + return nil +} + +func (m *Map) updateStates(ctx context.Context, task *modules.Task) error { + var toDelete []string + + m.Lock() + defer m.Unlock() + +pinLoop: + for _, pin := range m.all { + // Check for discontinued Hubs. + if m.intel != nil { + hubIntel, ok := m.intel.Hubs[pin.Hub.ID] + if ok && hubIntel.Discontinued { + toDelete = append(toDelete, pin.Hub.ID) + log.Infof("spn/navigator: deleting discontinued %s", pin.Hub) + continue pinLoop + } + } + // Check for obsoleted Hubs. + if pin.State.HasNoneOf(StateActive) && pin.Hub.Obsolete() { + toDelete = append(toDelete, pin.Hub.ID) + log.Infof("spn/navigator: deleting obsolete %s", pin.Hub) + } + + // Delete hubs async, as deleting triggers a couple hooks that lock the map. + if len(toDelete) > 0 { + module.StartWorker("delete hubs", func(_ context.Context) error { + for _, idToDelete := range toDelete { + err := hub.RemoveHubAndMsgs(m.Name, idToDelete) + if err != nil { + log.Warningf("spn/navigator: failed to delete Hub %s: %s", idToDelete, err) + } + } + return nil + }) + } + } + + // Update StateActive. + m.updateActiveHubs() + + // Update StateReachable. + return m.recalculateReachableHubs() +} + +// AddBootstrapHubs adds the given bootstrap hubs to the map. +func (m *Map) AddBootstrapHubs(bootstrapTransports []string) error { + m.Lock() + defer m.Unlock() + + return m.addBootstrapHubs(bootstrapTransports) +} + +func (m *Map) addBootstrapHubs(bootstrapTransports []string) error { + var anyAdded bool + var lastErr error + var failed int + for _, bootstrapTransport := range bootstrapTransports { + err := m.addBootstrapHub(bootstrapTransport) + if err != nil { + log.Warningf("spn/navigator: failed to add bootstrap hub %q to map %s: %s", bootstrapTransport, m.Name, err) + lastErr = err + failed++ + } else { + anyAdded = true + } + } + + if lastErr != nil && !anyAdded { + return lastErr + } + return nil +} + +func (m *Map) addBootstrapHub(bootstrapTransport string) error { + // Parse bootstrap hub. + transport, hubID, hubIP, err := hub.ParseBootstrapHub(bootstrapTransport) + if err != nil { + return fmt.Errorf("invalid bootstrap hub: %w", err) + } + + // Check if hub already exists. + var h *hub.Hub + pin, ok := m.all[hubID] + if ok { + h = pin.Hub + } else { + h = &hub.Hub{ + ID: hubID, + Map: m.Name, + Info: &hub.Announcement{ + ID: hubID, + }, + Status: &hub.Status{}, + FirstSeen: time.Now(), // Do not garbage collect bootstrap hubs. + } + } + + // Add IP if it does not yet exist. + if hubIP4 := hubIP.To4(); hubIP4 != nil { + if h.Info.IPv4 == nil { + h.Info.IPv4 = hubIP4 + } else if !h.Info.IPv4.Equal(hubIP4) { + return fmt.Errorf("additional bootstrap entry with same ID but mismatching IP address: %s", hubIP) + } + } else { + if h.Info.IPv6 == nil { + h.Info.IPv6 = hubIP + } else if !h.Info.IPv6.Equal(hubIP) { + return fmt.Errorf("additional bootstrap entry with same ID but mismatching IP address: %s", hubIP) + } + } + + // Add transport if it does not yet exist. + t := transport.String() + if !utils.StringInSlice(h.Info.Transports, t) { + h.Info.Transports = append(h.Info.Transports, t) + } + + // Add/update to map for bootstrapping. + m.updateHub(h, false, false) + log.Infof("spn/navigator: added/updated bootstrap %s to map %s", h, m.Name) + return nil +} + +// UpdateConfigQuickSettings updates config quick settings with available countries. +func (m *Map) UpdateConfigQuickSettings(ctx context.Context) error { + ctx, tracer := log.AddTracer(ctx) + tracer.Trace("navigator: updating SPN rules country quick settings") + defer tracer.Submit() + + opts := m.DefaultOptions() + opts.Home = &HomeHubOptions{ + Regard: StateTrusted, + } + opts.Destination = &DestinationHubOptions{ + Regard: StateTrusted, + Disregard: StateIsHomeHub, + } + + // Home Policy. + if err := m.updateQuickSettingExcludeCountryList(ctx, "spn/homePolicy", opts, HomeHub); err != nil { + return err + } + // Transit Policy. + if err := m.updateQuickSettingExcludeCountryList(ctx, profile.CfgOptionTransitHubPolicyKey, opts, TransitHub); err != nil { + return err + } + // Exit Policy. + if err := m.updateSelectRuleCountryList(ctx, profile.CfgOptionExitHubPolicyKey, opts, DestinationHub); err != nil { + return err + } + // DNS Exit Policy. + if err := m.updateSelectRuleCountryList(ctx, "spn/dnsExitPolicy", opts, DestinationHub); err != nil { + return err + } + + // Trust Nodes. + if err := m.updateQuickSettingVerifiedOwnerList(ctx, "spn/trustNodes"); err != nil { + return err + } + + tracer.Trace("navigator: finished updating SPN rules country quick settings") + return nil +} + +func (m *Map) updateQuickSettingExcludeCountryList(ctx context.Context, configKey string, opts *Options, matchFor HubType) error { + // Get config option. + cfgOption, err := config.GetOption(configKey) + if err != nil { + return fmt.Errorf("failed to get config option %s: %w", configKey, err) + } + + // Get list of countries for this config option. + countries := m.GetAvailableCountries(opts, matchFor) + // Convert to list. + countryList := make([]*geoip.CountryInfo, 0, len(countries)) + for _, country := range countries { + countryList = append(countryList, country) + } + // Sort list. + slices.SortFunc[[]*geoip.CountryInfo, *geoip.CountryInfo](countryList, func(a, b *geoip.CountryInfo) int { + return strings.Compare(a.Name, b.Name) + }) + + // Compile list of quick settings. + quickSettings := make([]config.QuickSetting, 0, len(countries)) + for _, country := range countryList { + quickSettings = append(quickSettings, config.QuickSetting{ + Name: fmt.Sprintf("Exclude %s (%s)", country.Name, country.Code), + Value: []string{fmt.Sprintf("- %s", country.Code)}, + Action: config.QuickMergeTop, + }) + } + + // Lock config option and set new quick settings. + cfgOption.Lock() + defer cfgOption.Unlock() + cfgOption.Annotations[config.QuickSettingsAnnotation] = quickSettings + + log.Tracer(ctx).Debugf("navigator: updated %d countries in quick settings for %s", len(quickSettings), configKey) + return nil +} + +type selectCountry struct { + config.QuickSetting + FlagID string +} + +func (m *Map) updateSelectRuleCountryList(ctx context.Context, configKey string, opts *Options, matchFor HubType) error { + // Get config option. + cfgOption, err := config.GetOption(configKey) + if err != nil { + return fmt.Errorf("failed to get config option %s: %w", configKey, err) + } + + // Get list of countries for this config option. + countries := m.GetAvailableCountries(opts, matchFor) + // Convert to list. + countryList := make([]*geoip.CountryInfo, 0, len(countries)) + for _, country := range countries { + countryList = append(countryList, country) + } + // Sort list. + slices.SortFunc[[]*geoip.CountryInfo, *geoip.CountryInfo](countryList, func(a, b *geoip.CountryInfo) int { + return strings.Compare(a.Name, b.Name) + }) + + // Get continents from countries. + continents := make(map[string]*geoip.ContinentInfo) + for _, country := range countryList { + continents[country.Continent.Code] = &country.Continent + } + // Convert to list. + continentList := make([]*geoip.ContinentInfo, 0, len(continents)) + for _, continent := range continents { + continentList = append(continentList, continent) + } + // Sort list. + slices.SortFunc[[]*geoip.ContinentInfo, *geoip.ContinentInfo](continentList, func(a, b *geoip.ContinentInfo) int { + return strings.Compare(a.Name, b.Name) + }) + + // Start compiling all options. + selections := make([]selectCountry, 0, len(continents)+len(countries)+2) + + // Add EU as special region. + selections = append(selections, selectCountry{ + QuickSetting: config.QuickSetting{ + Name: "European Union", + Value: []string{"+ AT", "+ BE", "+ BG", "+ CY", "+ CZ", "+ DE", "+ DK", "+ EE", "+ ES", "+ FI", "+ FR", "+ GR", "+ HR", "+ HU", "+ IE", "+ IT", "+ LT", "+ LU", "+ LV", "+ MT", "+ NL", "+ PL", "+ PT", "+ RO", "+ SE", "+ SI", "+ SK", "- *"}, + Action: config.QuickReplace, + }, + FlagID: "EU", + }) + selections = append(selections, selectCountry{ + QuickSetting: config.QuickSetting{ + Name: "US and Canada", + Value: []string{"+ US", "+ CA", "- *"}, + Action: config.QuickReplace, + }, + }) + + // Add countries to quick settings. + for _, country := range countryList { + selections = append(selections, selectCountry{ + QuickSetting: config.QuickSetting{ + Name: fmt.Sprintf("%s (%s)", country.Name, country.Code), + Value: []string{fmt.Sprintf("+ %s", country.Code), "- *"}, + Action: config.QuickReplace, + }, + FlagID: country.Code, + }) + } + + // Add continents to quick settings. + for _, continent := range continentList { + selections = append(selections, selectCountry{ + QuickSetting: config.QuickSetting{ + Name: fmt.Sprintf("%s (C:%s)", continent.Name, continent.Code), + Value: []string{fmt.Sprintf("+ C:%s", continent.Code), "- *"}, + Action: config.QuickReplace, + }, + }) + } + + // Lock config option and set new quick settings. + cfgOption.Lock() + defer cfgOption.Unlock() + cfgOption.Annotations[config.QuickSettingsAnnotation] = selections + + log.Tracer(ctx).Debugf("navigator: updated %d countries in quick settings for %s", len(selections), configKey) + return nil +} + +func (m *Map) updateQuickSettingVerifiedOwnerList(ctx context.Context, configKey string) error { + // Get config option. + cfgOption, err := config.GetOption(configKey) + if err != nil { + return fmt.Errorf("failed to get config option %s: %w", configKey, err) + } + + pins := m.pinList(true) + verifiedOwners := make([]string, 0, len(pins)/5) // Capacity is an estimation. + for _, pin := range pins { + pin.Lock() + vo := pin.VerifiedOwner + pin.Unlock() + + // Skip invalid/unneeded values. + switch vo { + case "", "Safing": + continue + } + + // Add to list, if not yet in there. + if !slices.Contains[[]string, string](verifiedOwners, vo) { + verifiedOwners = append(verifiedOwners, vo) + } + } + + // Sort list. + slices.Sort[[]string](verifiedOwners) + + // Compile list of quick settings. + quickSettings := make([]config.QuickSetting, 0, len(verifiedOwners)) + for _, vo := range verifiedOwners { + quickSettings = append(quickSettings, config.QuickSetting{ + Name: fmt.Sprintf("Trust %s", vo), + Value: []string{vo}, + Action: config.QuickMergeBottom, + }) + } + + // Lock config option and set new quick settings. + cfgOption.Lock() + defer cfgOption.Unlock() + cfgOption.Annotations[config.QuickSettingsAnnotation] = quickSettings + + log.Tracer(ctx).Debugf("navigator: updated %d verified owners in quick settings for %s", len(quickSettings), configKey) + return nil +} diff --git a/spn/patrol/domains.go b/spn/patrol/domains.go new file mode 100644 index 00000000..43fff823 --- /dev/null +++ b/spn/patrol/domains.go @@ -0,0 +1,311 @@ +package patrol + +import ( + "math/rand" + "time" +) + +// getRandomTestDomain returns a random test domain from the test domain list. +// Not cryptographically secure random, though. +func getRandomTestDomain() string { + rng := rand.New(rand.NewSource(time.Now().UnixNano())) //nolint:gosec + return testDomains[rng.Intn(len(testDomains)-1)] //nolint:gosec // Weak randomness is not an issue here. +} + +// testDomains is a list of domains to check if they respond successfully to a HTTP GET request. +// They are sourced from tranco - trimmed, checked, and cleaned. +// Use TestCleanDomains to clean a new/updated list. +// Treat as a constant. +var testDomains = []string{ + "about.com", + "addtoany.com", + "adobe.com", + "aliyun.com", + "ampproject.org", + "android.com", + "apache.org", + "apple.com", + "apple.news", + "appspot.com", + "arnebrachhold.de", + "avast.com", + "bbc.co.uk", + "bbc.com", + "bing.com", + "blogger.com", + "blogspot.com", + "branch.io", + "calendly.com", + "cam.ac.uk", + "canonical.com", + "canva.com", + "cisco.com", + "cloudflare.com", + "cloudns.net", + "cnblogs.com", + "cnn.com", + "creativecommons.org", + "criteo.com", + "cupfox.app", + "dailymail.co.uk", + "ddnss.de", + "debian.org", + "digitalocean.com", + "doi.org", + "domainmarket.com", + "doubleclick.net", + "dreamhost.com", + "dropbox.com", + "dynect.net", + "ed.gov", + "elegantthemes.com", + "elpais.com", + "epa.gov", + "eporner.com", + "espn.com", + "europa.eu", + "example.com", + "facebook.com", + "fb.com", + "fb.me", + "fb.watch", + "fbcdn.net", + "feedburner.com", + "free.fr", + "ftc.gov", + "g.page", + "getbootstrap.com", + "gitlab.com", + "gmail.com", + "gnu.org", + "goo.gl", + "google-analytics.com", + "google.ca", + "google.co.in", + "google.co.jp", + "google.co.th", + "google.co.uk", + "google.com.au", + "google.com.br", + "google.com.hk", + "google.com.mx", + "google.com.tr", + "google.com.tw", + "google.com", + "google.de", + "google.es", + "google.fr", + "google.it", + "googledomains.com", + "googlesyndication.com", + "gstatic.com", + "harvard.edu", + "hitomi.la", + "hubspot.com", + "hugedomains.com", + "ibm.com", + "icloud.com", + "ikea.com", + "ilovepdf.com", + "indiatimes.com", + "instagram.com", + "investing.com", + "investopedia.com", + "irs.gov", + "kickstarter.com", + "launchpad.net", + "lencr.org", + "lijit.com", + "linkedin.com", + "linode.com", + "mashable.com", + "medium.com", + "mega.co.nz", + "mega.nz", + "merriam-webster.com", + "mit.edu", + "netflix.com", + "nginx.org", + "nist.gov", + "notion.so", + "nsone.net", + "office.com", + "onetrust.com", + "openstreetmap.org", + "patreon.com", + "pexels.com", + "photobucket.com", + "php.net", + "pki.goog", + "plos.org", + "ps.kz", + "readthedocs.io", + "redd.it", + "reddit.com", + "remove.bg", + "rfc-editor.org", + "savefrom.net", + "sedo.com", + "so-net.ne.jp", + "sourceforge.net", + "spamhaus.org", + "speedtest.net", + "spotify.com", + "stanford.edu", + "state.gov", + "substack.com", + "t.me", + "taboola.com", + "techcrunch.com", + "telegram.me", + "telegram.org", + "threema.ch", + "tinyurl.com", + "ubuntu.com", + "ui.com", + "umich.edu", + "uol.com.br", + "upenn.edu", + "usgs.gov", + "utexas.edu", + "va.gov", + "verisign.com", + "vmware.com", + "w3.org", + "wa.me", + "webs.com", + "whatsapp.com", + "whatsapp.net", + "whitehouse.gov", + "wikimedia.org", + "wikipedia.org", + "wiktionary.org", + "www.aliyundrive.com", + "www.amazon.ca", + "www.amazon.co.jp", + "www.amazon.co.uk", + "www.amazon.com", + "www.amazon.de", + "www.amazon.es", + "www.amazon.fr", + "www.amazon.in", + "www.amazon.it", + "www.aol.com", + "www.appsflyer.com", + "www.att.com", + "www.business.site", + "www.ca.gov", + "www.canada.ca", + "www.cctv.com", + "www.cdc.gov", + "www.chinaz.com", + "www.cloud.com", + "www.cnet.com", + "www.comcast.com", + "www.comcast.net", + "www.cornell.edu", + "www.crashlytics.com", + "www.datadoghq.com", + "www.db.com", + "www.deloitte.com", + "www.dw.com", + "www.engadget.com", + "www.eset.com", + "www.fao.org", + "www.fedex.com", + "www.flickr.com", + "www.force.com", + "www.ford.com", + "www.frontiersin.org", + "www.geeksforgeeks.org", + "www.gene.com", + "www.genius.com", + "www.github.io", + "www.gov.uk", + "www.gravatar.com", + "www.healthline.com", + "www.hhs.gov", + "www.hichina.com", + "www.hinet.net", + "www.house.gov", + "www.hp.com", + "www.huawei.com", + "www.hupu.com", + "www.ietf.org", + "www.immunet.com", + "www.independent.co.uk", + "www.intel.com", + "www.jotform.com", + "www.klaviyo.com", + "www.launchdarkly.com", + "www.live.com", + "www.macromedia.com", + "www.medallia.com", + "www.mediatek.com", + "www.medicalnewstoday.com", + "www.microsoft.com", + "www.mongodb.com", + "www.mysql.com", + "www.namu.wiki", + "www.nasa.gov", + "www.nba.com", + "www.nbcnews.com", + "www.nih.gov", + "www.noaa.gov", + "www.npr.org", + "www.nps.gov", + "www.ny.gov", + "www.okta.com", + "www.openai.com", + "www.optimizely.com", + "www.oracle.com", + "www.outlook.com", + "www.paloaltonetworks.com", + "www.pbs.org", + "www.pixabay.com", + "www.plala.or.jp", + "www.playstation.com", + "www.plesk.com", + "www.princeton.edu", + "www.prnewswire.com", + "www.psu.edu", + "www.python.org", + "www.qq.com", + "www.quantserve.com", + "www.quillbot.com", + "www.rackspace.com", + "www.redhat.com", + "www.researchgate.net", + "www.roku.com", + "www.salesforce.com", + "www.skype.com", + "www.sun.com", + "www.teamviewer.com", + "www.ted.com", + "www.tesla.com", + "www.theguardian.com", + "www.typeform.com", + "www.uchicago.edu", + "www.ucla.edu", + "www.usda.gov", + "www.usps.com", + "www.utorrent.com", + "www.warnerbros.com", + "www.webex.com", + "www.who.int", + "www.worldbank.org", + "www.xbox.com", + "www.xerox.com", + "www.youdao.com", + "www.zdnet.com", + "www.zebra.com", + "yahoo.com", + "yale.edu", + "yandex.com", + "yandex.net", + "youku.com", + "youtu.be", + "youtube.com", + "zemanta.com", + "zoro.to", +} diff --git a/spn/patrol/domains_test.go b/spn/patrol/domains_test.go new file mode 100644 index 00000000..a5e28895 --- /dev/null +++ b/spn/patrol/domains_test.go @@ -0,0 +1,67 @@ +package patrol + +import ( + "context" + "fmt" + "sort" + "testing" +) + +var enableDomainTools = "no" // change to "yes" to enable + +// TestCleanDomains checks, cleans and prints an improved domain list. +// Run with: +// go test -run ^TestCleanDomains$ github.com/safing/portmaster/spn/patrol -ldflags "-X github.com/safing/portmaster/spn/patrol.enableDomainTools=yes" -timeout 3h -v +// This is provided as a test for easier maintenance and ops. +func TestCleanDomains(t *testing.T) { //nolint:paralleltest + if enableDomainTools != "yes" { + t.Skip() + return + } + + // Setup context. + ctx := context.Background() + + // Go through all domains and check if they are reachable. + goodDomains := make([]string, 0, len(testDomains)) + for _, domain := range testDomains { + // Check if domain is reachable. + code, err := domainIsUsable(ctx, domain) + if err != nil { + fmt.Printf("FAIL: %s: %s\n", domain, err) + } else { + fmt.Printf("OK: %s [%d]\n", domain, code) + goodDomains = append(goodDomains, domain) + continue + } + + // If failed, try again with a www. prefix + wwwDomain := "www." + domain + code, err = domainIsUsable(ctx, wwwDomain) + if err != nil { + fmt.Printf("FAIL: %s: %s\n", wwwDomain, err) + } else { + fmt.Printf("OK: %s [%d]\n", wwwDomain, code) + goodDomains = append(goodDomains, wwwDomain) + } + + } + + sort.Strings(goodDomains) + fmt.Println("printing good domains:") + for _, domain := range goodDomains { + fmt.Printf("%q,\n", domain) + } + + fmt.Println("IMPORTANT: do not forget to go through list and check if everything looks good") +} + +func domainIsUsable(ctx context.Context, domain string) (statusCode int, err error) { + // Try IPv6 first as it is way more likely to fail. + statusCode, err = CheckHTTPSConnection(ctx, "tcp6", domain) + if err != nil { + return + } + + return CheckHTTPSConnection(ctx, "tcp4", domain) +} diff --git a/spn/patrol/http.go b/spn/patrol/http.go new file mode 100644 index 00000000..391518c1 --- /dev/null +++ b/spn/patrol/http.go @@ -0,0 +1,186 @@ +package patrol + +import ( + "context" + "fmt" + "net" + "net/http" + "time" + + "github.com/tevino/abool" + + "github.com/safing/portbase/log" + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/spn/conf" +) + +var httpsConnectivityConfirmed = abool.NewBool(true) + +// HTTPSConnectivityConfirmed returns whether the last HTTPS connectivity check succeeded. +// Is "true" before first test. +func HTTPSConnectivityConfirmed() bool { + return httpsConnectivityConfirmed.IsSet() +} + +func connectivityCheckTask(ctx context.Context, task *modules.Task) error { + // Start tracing logs. + ctx, tracer := log.AddTracer(ctx) + defer tracer.Submit() + + // Run checks and report status. + success := runConnectivityChecks(ctx) + if success { + tracer.Info("spn/patrol: all connectivity checks succeeded") + if httpsConnectivityConfirmed.SetToIf(false, true) { + module.TriggerEvent(ChangeSignalEventName, nil) + } + return nil + } + + tracer.Errorf("spn/patrol: connectivity check failed") + if httpsConnectivityConfirmed.SetToIf(true, false) { + module.TriggerEvent(ChangeSignalEventName, nil) + } + return nil +} + +func runConnectivityChecks(ctx context.Context) (ok bool) { + switch { + case conf.HubHasIPv4() && !runHTTPSConnectivityChecks(ctx, "tcp4"): + return false + case conf.HubHasIPv6() && !runHTTPSConnectivityChecks(ctx, "tcp6"): + return false + default: + // All checks passed. + return true + } +} + +func runHTTPSConnectivityChecks(ctx context.Context, network string) (ok bool) { + // Step 1: Check 1 domain, require 100% + if checkHTTPSConnectivity(ctx, network, 1, 1) { + return true + } + + // Step 2: Check 5 domains, require 80% + if checkHTTPSConnectivity(ctx, network, 5, 0.8) { + return true + } + + // Step 3: Check 20 domains, require 70% + if checkHTTPSConnectivity(ctx, network, 20, 0.7) { + return true + } + + return false +} + +func checkHTTPSConnectivity(ctx context.Context, network string, checks int, requiredSuccessFraction float32) (ok bool) { + log.Tracer(ctx).Tracef( + "spn/patrol: testing connectivity via https (%d checks; %.0f%% required)", + checks, + requiredSuccessFraction*100, + ) + + // Run tests. + var succeeded int + for i := 0; i < checks; i++ { + if checkHTTPSConnection(ctx, network) { + succeeded++ + } + } + + // Check success. + successFraction := float32(succeeded) / float32(checks) + if successFraction < requiredSuccessFraction { + log.Tracer(ctx).Warningf( + "spn/patrol: https/%s connectivity check failed: %d/%d (%.0f%%)", + network, + succeeded, + checks, + successFraction*100, + ) + return false + } + + log.Tracer(ctx).Debugf( + "spn/patrol: https/%s connectivity check succeeded: %d/%d (%.0f%%)", + network, + succeeded, + checks, + successFraction*100, + ) + return true +} + +func checkHTTPSConnection(ctx context.Context, network string) (ok bool) { + testDomain := getRandomTestDomain() + code, err := CheckHTTPSConnection(ctx, network, testDomain) + if err != nil { + log.Tracer(ctx).Debugf("spn/patrol: https/%s connect check failed: %s: %s", network, testDomain, err) + return false + } + + log.Tracer(ctx).Tracef("spn/patrol: https/%s connect check succeeded: %s [%d]", network, testDomain, code) + return true +} + +// CheckHTTPSConnection checks if a HTTPS connection to the given domain can be established. +func CheckHTTPSConnection(ctx context.Context, network, domain string) (statusCode int, err error) { + // Check network parameter. + switch network { + case "tcp4": + case "tcp6": + default: + return 0, fmt.Errorf("provided unsupported network: %s", network) + } + + // Build URL. + // Use HTTPS to ensure that we have really communicated with the desired + // server and not with an intermediate. + url := fmt.Sprintf("https://%s/", domain) + + // Prepare all parts of the request. + // TODO: Evaluate if we want to change the User-Agent. + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return 0, err + } + dialer := &net.Dialer{ + Timeout: 15 * time.Second, + LocalAddr: conf.GetBindAddr(network), + FallbackDelay: -1, // Disables Fast Fallback from IPv6 to IPv4. + KeepAlive: -1, // Disable keep-alive. + } + dialWithNet := func(ctx context.Context, _, addr string) (net.Conn, error) { + // Ignore network by http client. + // Instead, force either tcp4 or tcp6. + return dialer.DialContext(ctx, network, addr) + } + client := &http.Client{ + Transport: &http.Transport{ + DialContext: dialWithNet, + DisableKeepAlives: true, + DisableCompression: true, + TLSHandshakeTimeout: 15 * time.Second, + }, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + Timeout: 30 * time.Second, + } + + // Make request to server. + resp, err := client.Do(req) + if err != nil { + return 0, fmt.Errorf("failed to send http request: %w", err) + } + defer func() { + _ = resp.Body.Close() + }() + if resp.StatusCode < 200 || resp.StatusCode >= 400 { + return resp.StatusCode, fmt.Errorf("unexpected status code: %s", resp.Status) + } + + return resp.StatusCode, nil +} diff --git a/spn/patrol/module.go b/spn/patrol/module.go new file mode 100644 index 00000000..842c139c --- /dev/null +++ b/spn/patrol/module.go @@ -0,0 +1,32 @@ +package patrol + +import ( + "time" + + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/spn/conf" +) + +// ChangeSignalEventName is the name of the event that signals any change in the patrol system. +const ChangeSignalEventName = "change signal" + +var module *modules.Module + +func init() { + module = modules.Register("patrol", prep, start, nil, "rng") +} + +func prep() error { + module.RegisterEvent(ChangeSignalEventName, false) + + return nil +} + +func start() error { + if conf.PublicHub() { + module.NewTask("connectivity test", connectivityCheckTask). + Repeat(5 * time.Minute) + } + + return nil +} diff --git a/spn/ships/connection_test.go b/spn/ships/connection_test.go new file mode 100644 index 00000000..5d03927b --- /dev/null +++ b/spn/ships/connection_test.go @@ -0,0 +1,131 @@ +package ships + +import ( + "context" + "fmt" + "net" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/safing/portmaster/spn/hub" +) + +var ( + testPort uint16 = 65000 + testData = []byte("The quick brown fox jumps over the lazy dog") + localhost = net.IPv4(127, 0, 0, 1) +) + +func getTestPort() uint16 { + testPort++ + return testPort +} + +func getTestBuf() []byte { + return make([]byte, len(testData)) +} + +func TestConnections(t *testing.T) { + t.Parallel() + + registryLock.Lock() + t.Cleanup(func() { + registryLock.Unlock() + }) + + for k, v := range registry { //nolint:paralleltest // False positive. + protocol, builder := k, v + t.Run(protocol, func(t *testing.T) { + t.Parallel() + + var wg sync.WaitGroup + ctx, cancelCtx := context.WithCancel(context.Background()) + + // docking requests + dockingRequests := make(chan Ship, 1) + transport := &hub.Transport{ + Protocol: protocol, + Port: getTestPort(), + } + + // create listener + pier, err := builder.EstablishPier(transport, dockingRequests) + if err != nil { + t.Fatal(err) + } + + // connect to listener + ship, err := builder.LaunchShip(ctx, transport, localhost) + if err != nil { + t.Fatal(err) + } + + // client send + err = ship.Load(testData) + if err != nil { + t.Fatalf("%s failed: %s", ship, err) + } + + // dock client + srvShip := <-dockingRequests + if srvShip == nil { + t.Fatalf("%s failed to dock", pier) + } + + // server recv + buf := getTestBuf() + _, err = srvShip.UnloadTo(buf) + if err != nil { + t.Fatalf("%s failed: %s", ship, err) + } + + // check data + assert.Equal(t, testData, buf, "should match") + fmt.Print(".") + + for i := 0; i < 100; i++ { + // server send + err = srvShip.Load(testData) + if err != nil { + t.Fatalf("%s failed: %s", ship, err) + } + + // client recv + buf = getTestBuf() + _, err = ship.UnloadTo(buf) + if err != nil { + t.Fatalf("%s failed: %s", ship, err) + } + + // check data + assert.Equal(t, testData, buf, "should match") + fmt.Print(".") + + // client send + err = ship.Load(testData) + if err != nil { + t.Fatalf("%s failed: %s", ship, err) + } + + // server recv + buf = getTestBuf() + _, err = srvShip.UnloadTo(buf) + if err != nil { + t.Fatalf("%s failed: %s", ship, err) + } + + // check data + assert.Equal(t, testData, buf, "should match") + fmt.Print(".") + } + + ship.Sink() + srvShip.Sink() + pier.Abolish() + cancelCtx() + wg.Wait() // wait for docking procedure to end + }) + } +} diff --git a/spn/ships/http.go b/spn/ships/http.go new file mode 100644 index 00000000..165ca9df --- /dev/null +++ b/spn/ships/http.go @@ -0,0 +1,230 @@ +package ships + +import ( + "bufio" + "context" + "fmt" + "io" + "net" + "net/http" + "time" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/hub" +) + +// HTTPShip is a ship that uses HTTP. +type HTTPShip struct { + ShipBase +} + +// HTTPPier is a pier that uses HTTP. +type HTTPPier struct { + PierBase + + newDockings chan net.Conn +} + +func init() { + Register("http", &Builder{ + LaunchShip: launchHTTPShip, + EstablishPier: establishHTTPPier, + }) +} + +/* +HTTP Transport Variants: + +1. Hijack connection and switch to raw SPN protocol: + +Request: + + GET HTTP/1.1 + Connection: Upgrade + Upgrade: SPN + +Response: + + HTTP/1.1 101 Switching Protocols + Connection: Upgrade + Upgrade: SPN + +*/ + +func launchHTTPShip(ctx context.Context, transport *hub.Transport, ip net.IP) (Ship, error) { + // Default to root path. + path := transport.Path + if path == "" { + path = "/" + } + + // Build request for Variant 1. + variant := 1 + request, err := http.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, fmt.Errorf("failed to build HTTP request: %w", err) + } + request.Header.Set("Connection", "Upgrade") + request.Header.Set("Upgrade", "SPN") + + // Create connection. + var dialNet string + if ip4 := ip.To4(); ip4 != nil { + dialNet = "tcp4" + } else { + dialNet = "tcp6" + } + dialer := &net.Dialer{ + Timeout: 30 * time.Second, + LocalAddr: conf.GetBindAddr(dialNet), + FallbackDelay: -1, // Disables Fast Fallback from IPv6 to IPv4. + KeepAlive: -1, // Disable keep-alive. + } + conn, err := dialer.DialContext(ctx, dialNet, net.JoinHostPort(ip.String(), portToA(transport.Port))) + if err != nil { + return nil, fmt.Errorf("failed to connect: %w", err) + } + + // Send HTTP request. + err = request.Write(conn) + if err != nil { + return nil, fmt.Errorf("failed to send HTTP request: %w", err) + } + + // Receive HTTP response. + response, err := http.ReadResponse(bufio.NewReader(conn), request) + if err != nil { + return nil, fmt.Errorf("failed to read HTTP response: %w", err) + } + defer response.Body.Close() //nolint:errcheck,gosec + + // Handle response according to variant. + switch variant { + case 1: + if response.StatusCode == http.StatusSwitchingProtocols && + response.Header.Get("Connection") == "Upgrade" && + response.Header.Get("Upgrade") == "SPN" { + // Continue + } else { + return nil, fmt.Errorf("received unexpected response for variant 1: %s", response.Status) + } + + default: + return nil, fmt.Errorf("internal error: unsupported http transport variant: %d", variant) + } + + // Create ship. + ship := &HTTPShip{ + ShipBase: ShipBase{ + conn: conn, + transport: transport, + mine: true, + secure: false, + }, + } + + // Init and return. + ship.calculateLoadSize(ip, nil, TCPHeaderMTUSize) + ship.initBase() + return ship, nil +} + +func (pier *HTTPPier) ServeHTTP(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && + r.Header.Get("Connection") == "Upgrade" && + r.Header.Get("Upgrade") == "SPN": + // Request for Variant 1. + + // Hijack connection. + var conn net.Conn + if hijacker, ok := w.(http.Hijacker); ok { + // Empty body, so the hijacked connection starts with a clean buffer. + _, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "", http.StatusInternalServerError) + log.Warningf("ships: failed to empty body for hijack for %s: %s", r.RemoteAddr, err) + return + } + _ = r.Body.Close() + + // Reply with upgrade confirmation. + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "SPN") + w.WriteHeader(http.StatusSwitchingProtocols) + + // Get connection. + conn, _, err = hijacker.Hijack() + if err != nil { + log.Warningf("ships: failed to hijack http connection from %s: %s", r.RemoteAddr, err) + return + } + } else { + http.Error(w, "", http.StatusInternalServerError) + log.Warningf("ships: connection from %s cannot be hijacked", r.RemoteAddr) + return + } + + // Create new ship. + ship := &HTTPShip{ + ShipBase: ShipBase{ + transport: pier.transport, + conn: conn, + mine: false, + secure: false, + }, + } + ship.calculateLoadSize(nil, conn.RemoteAddr(), TCPHeaderMTUSize) + ship.initBase() + + // Submit new docking request. + select { + case pier.dockingRequests <- ship: + case <-r.Context().Done(): + return + } + + default: + // Reply with info page if no variant matches the request. + ServeInfoPage(w, r) + } +} + +func establishHTTPPier(transport *hub.Transport, dockingRequests chan Ship) (Pier, error) { + // Default to root path. + path := transport.Path + if path == "" { + path = "/" + } + + // Create pier. + pier := &HTTPPier{ + newDockings: make(chan net.Conn), + PierBase: PierBase{ + transport: transport, + dockingRequests: dockingRequests, + }, + } + pier.initBase() + + // Register handler. + err := addHTTPHandler(transport.Port, path, pier.ServeHTTP) + if err != nil { + return nil, fmt.Errorf("failed to add HTTP handler: %w", err) + } + + return pier, nil +} + +// Abolish closes the underlying listener and cleans up any related resources. +func (pier *HTTPPier) Abolish() { + // Only abolish once. + if !pier.abolishing.SetToIf(false, true) { + return + } + + // Do not close the listener, as it is shared. + // Instead, remove the HTTP handler and the shared server will shutdown itself when needed. + _ = removeHTTPHandler(pier.transport.Port, pier.transport.Path) +} diff --git a/spn/ships/http_info.go b/spn/ships/http_info.go new file mode 100644 index 00000000..886f2127 --- /dev/null +++ b/spn/ships/http_info.go @@ -0,0 +1,83 @@ +package ships + +import ( + "bytes" + _ "embed" + "html/template" + "net/http" + + "github.com/safing/portbase/config" + "github.com/safing/portbase/info" + "github.com/safing/portbase/log" +) + +var ( + //go:embed http_info_page.html.tmpl + infoPageData string + + infoPageTemplate *template.Template + + // DisplayHubID holds the Hub ID for displaying it on the info page. + DisplayHubID string +) + +type infoPageInput struct { + Version string + Info *info.Info + ID string + Name string + Group string + ContactAddress string + ContactService string +} + +var ( + pageInputName config.StringOption + pageInputGroup config.StringOption + pageInputContactAddress config.StringOption + pageInputContactService config.StringOption +) + +func initPageInput() { + infoPageTemplate = template.Must(template.New("info-page").Parse(infoPageData)) + + pageInputName = config.Concurrent.GetAsString("spn/publicHub/name", "") + pageInputGroup = config.Concurrent.GetAsString("spn/publicHub/group", "") + pageInputContactAddress = config.Concurrent.GetAsString("spn/publicHub/contactAddress", "") + pageInputContactService = config.Concurrent.GetAsString("spn/publicHub/contactService", "") +} + +// ServeInfoPage serves the info page for the given request. +func ServeInfoPage(w http.ResponseWriter, r *http.Request) { + pageData, err := renderInfoPage() + if err != nil { + log.Warningf("ships: failed to render SPN info page: %s", err) + http.Error(w, "", http.StatusInternalServerError) + return + } + + _, err = w.Write(pageData) + if err != nil { + log.Warningf("ships: failed to write info page: %s", err) + } +} + +func renderInfoPage() ([]byte, error) { + input := &infoPageInput{ + Version: info.Version(), + Info: info.GetInfo(), + ID: DisplayHubID, + Name: pageInputName(), + Group: pageInputGroup(), + ContactAddress: pageInputContactAddress(), + ContactService: pageInputContactService(), + } + + buf := &bytes.Buffer{} + err := infoPageTemplate.ExecuteTemplate(buf, "info-page", input) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/spn/ships/http_info_page.html.tmpl b/spn/ships/http_info_page.html.tmpl new file mode 100644 index 00000000..5a4805ed --- /dev/null +++ b/spn/ships/http_info_page.html.tmpl @@ -0,0 +1,112 @@ + + + + + + SPN Node + + + +
+
+

+ You Have Reached an SPN Node +

+

+ The server, or at least the exact URL you have accessed, leads to an SPN Node. +

+
+ +
+

+ What is SPN? +

+

+ SPN stands for "Safing Privacy Network" and is a network of servers that offers high privacy protection of Internet traffic and activity. It was built to replace VPNs for their Internet privacy use case. +

+
+ +
+

+ More Information +

+

+ You can find out more about SPN here: +

+

+
+ +
+

+ Contact the Operator of This SPN Node +

+

+ {{ if .ContactAddress }} + You can reach the operator of this SPN Node here: + {{ .ContactAddress }} + {{ if .ContactService }} via {{ .ContactService }} + {{ end }} + {{ else }} + The operator of this SPN Node has not configured any contact data.
+ Please contact the operator using the usual methods via the hosting provider. + {{ end }} +

+
+ +
+

+ Are You Tracing Bad Activity? +

+

+ We are sorry there is an incident involving this server. We condemn any disruptive or illegal activity. +

+

+ Please note that servers are not only operated by Safing (the company behind SPN), but also by third parties. +

+

+ The SPN works very similar to Tor. Its primary goal is to provide people more privacy on the Internet. We also provide our services to people behind censoring firewalls in oppressive regimes. +

+

+ This server does not host any content (as part of its role in the SPN network). Rather, it is part of the network where nodes on the Internet simply pass packets among themselves before sending them to their destinations, just as any Internet intermediary does. +

+

+ Please understand that the SPN makes it technically impossible to single out individual users. We are also legally bound to respective privacy rights. +

+

+ We can offer to block specific destination IPs and ports, but the abuser doesn't use this server specifically; instead, they will just be routed through a different exit node outside of our control. +

+
+ +
+

+ SPN Node Info +

+

+

    +
  • Name: {{ .Name }}
  • +
  • Group: {{ .Group }}
  • +
  • ContactAddress: {{ .ContactAddress }}
  • +
  • ContactService: {{ .ContactService }}
  • +
  • Version: {{ .Version }}
  • +
  • ID: {{ .ID }}
  • +
  • + Build: +
      +
    • Commit: {{ .Info.Commit }}
    • +
    • Host: {{ .Info.BuildHost }}
    • +
    • Date: {{ .Info.BuildDate }}
    • +
    • Source: {{ .Info.BuildSource }}
    • +
    +
  • +
+

+
+
+ + diff --git a/spn/ships/http_info_test.go b/spn/ships/http_info_test.go new file mode 100644 index 00000000..a490dfce --- /dev/null +++ b/spn/ships/http_info_test.go @@ -0,0 +1,26 @@ +package ships + +import ( + "html/template" + "testing" + + "github.com/safing/portbase/config" +) + +func TestInfoPageTemplate(t *testing.T) { + t.Parallel() + + infoPageTemplate = template.Must(template.New("info-page").Parse(infoPageData)) + pageInputName = config.Concurrent.GetAsString("spn/publicHub/name", "node-name") + pageInputGroup = config.Concurrent.GetAsString("spn/publicHub/group", "node-group") + pageInputContactAddress = config.Concurrent.GetAsString("spn/publicHub/contactAddress", "john@doe.com") + pageInputContactService = config.Concurrent.GetAsString("spn/publicHub/contactService", "email") + + pageData, err := renderInfoPage() + if err != nil { + t.Fatal(err) + } + + _ = pageData + // t.Log(string(pageData)) +} diff --git a/spn/ships/http_shared.go b/spn/ships/http_shared.go new file mode 100644 index 00000000..c90504e1 --- /dev/null +++ b/spn/ships/http_shared.go @@ -0,0 +1,188 @@ +package ships + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "sync" + "time" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/conf" +) + +type sharedServer struct { + server *http.Server + + handlers map[string]http.HandlerFunc + handlersLock sync.RWMutex +} + +// ServeHTTP forwards requests to registered handler or uses defaults. +func (shared *sharedServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + shared.handlersLock.Lock() + defer shared.handlersLock.Unlock() + + // Get and forward to registered handler. + handler, ok := shared.handlers[r.URL.Path] + if ok { + handler(w, r) + return + } + + // If there is registered handler and path is "/", respond with info page. + if r.Method == http.MethodGet && r.URL.Path == "/" { + ServeInfoPage(w, r) + return + } + + // Otherwise, respond with error. + http.Error(w, "", http.StatusNotFound) +} + +var ( + sharedHTTPServers = make(map[uint16]*sharedServer) + sharedHTTPServersLock sync.Mutex +) + +func addHTTPHandler(port uint16, path string, handler http.HandlerFunc) error { + // Check params. + if port == 0 { + return errors.New("cannot listen on port 0") + } + + // Default to root path. + if path == "" { + path = "/" + } + + sharedHTTPServersLock.Lock() + defer sharedHTTPServersLock.Unlock() + + // Get http server of the port. + shared, ok := sharedHTTPServers[port] + if ok { + // Set path to handler. + shared.handlersLock.Lock() + defer shared.handlersLock.Unlock() + + // Check if path is already registered. + _, ok := shared.handlers[path] + if ok { + return errors.New("path already registered") + } + + // Else, register handler at path. + shared.handlers[path] = handler + return nil + } + + // Shared server does not exist - create one. + shared = &sharedServer{ + handlers: make(map[string]http.HandlerFunc), + } + + // Add first handler. + shared.handlers[path] = handler + + // Define new server. + server := &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: shared, + ReadTimeout: 1 * time.Minute, + ReadHeaderTimeout: 10 * time.Second, + WriteTimeout: 1 * time.Minute, + IdleTimeout: 1 * time.Minute, + MaxHeaderBytes: 4096, + // ErrorLog: &log.Logger{}, // FIXME + BaseContext: func(net.Listener) context.Context { return module.Ctx }, + } + shared.server = server + + // Start listeners. + bindIPs := conf.GetBindIPs() + listeners := make([]net.Listener, 0, len(bindIPs)) + for _, bindIP := range bindIPs { + listener, err := net.ListenTCP("tcp", &net.TCPAddr{ + IP: bindIP, + Port: int(port), + }) + if err != nil { + return fmt.Errorf("failed to listen: %w", err) + } + + listeners = append(listeners, listener) + log.Infof("spn/ships: http transport pier established on %s", listener.Addr()) + } + + // Add shared http server to list. + sharedHTTPServers[port] = shared + + // Start servers in service workers. + for _, listener := range listeners { + serviceListener := listener + module.StartServiceWorker( + fmt.Sprintf("shared http server listener on %s", listener.Addr()), 0, + func(ctx context.Context) error { + err := shared.server.Serve(serviceListener) + if !errors.Is(http.ErrServerClosed, err) { + return err + } + return nil + }, + ) + } + + return nil +} + +func removeHTTPHandler(port uint16, path string) error { + // Check params. + if port == 0 { + return nil + } + + // Default to root path. + if path == "" { + path = "/" + } + + sharedHTTPServersLock.Lock() + defer sharedHTTPServersLock.Unlock() + + // Get http server of the port. + shared, ok := sharedHTTPServers[port] + if !ok { + return nil + } + + // Set path to handler. + shared.handlersLock.Lock() + defer shared.handlersLock.Unlock() + + // Check if path is registered. + _, ok = shared.handlers[path] + if !ok { + return nil + } + + // Remove path from handler. + delete(shared.handlers, path) + + // Shutdown shared HTTP server if no more handlers are registered. + if len(shared.handlers) == 0 { + ctx, cancel := context.WithTimeout( + context.Background(), + 10*time.Second, + ) + defer cancel() + return shared.server.Shutdown(ctx) + } + + // Remove shared HTTP server from map. + delete(sharedHTTPServers, port) + + return nil +} diff --git a/spn/ships/http_shared_test.go b/spn/ships/http_shared_test.go new file mode 100644 index 00000000..e16ff53d --- /dev/null +++ b/spn/ships/http_shared_test.go @@ -0,0 +1,33 @@ +package ships + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSharedHTTP(t *testing.T) { //nolint:paralleltest // Test checks global state. + const testPort = 65100 + + // Register multiple handlers. + err := addHTTPHandler(testPort, "", ServeInfoPage) + assert.NoError(t, err, "should be able to share http listener") + err = addHTTPHandler(testPort, "/test", ServeInfoPage) + assert.NoError(t, err, "should be able to share http listener") + err = addHTTPHandler(testPort, "/test2", ServeInfoPage) + assert.NoError(t, err, "should be able to share http listener") + err = addHTTPHandler(testPort, "/", ServeInfoPage) + assert.Error(t, err, "should fail to register path twice") + + // Unregister + assert.NoError(t, removeHTTPHandler(testPort, "")) + assert.NoError(t, removeHTTPHandler(testPort, "/test")) + assert.NoError(t, removeHTTPHandler(testPort, "/not-registered")) // removing unregistered handler does not error + assert.NoError(t, removeHTTPHandler(testPort, "/test2")) + assert.NoError(t, removeHTTPHandler(testPort, "/not-registered")) // removing unregistered handler does not error + + // Check if all handlers are gone again. + sharedHTTPServersLock.Lock() + defer sharedHTTPServersLock.Unlock() + assert.Equal(t, 0, len(sharedHTTPServers), "shared http handlers should be back to zero") +} diff --git a/spn/ships/kcp.go b/spn/ships/kcp.go new file mode 100644 index 00000000..88bfb2ad --- /dev/null +++ b/spn/ships/kcp.go @@ -0,0 +1,81 @@ +package ships + +// KCPShip is a ship that uses KCP. +type KCPShip struct { + ShipBase +} + +// KCPPier is a pier that uses KCP. +type KCPPier struct { + PierBase +} + +// TODO: Find a replacement for kcp, which turned out to not fit our use case. +/* +func init() { + Register("kcp", &Builder{ + LaunchShip: launchKCPShip, + EstablishPier: establishKCPPier, + }) +} + +func launchKCPShip(ctx context.Context, transport *hub.Transport, ip net.IP) (Ship, error) { + conn, err := kcp.Dial(net.JoinHostPort(ip.String(), portToA(transport.Port))) + if err != nil { + return nil, err + } + + ship := &KCPShip{ + ShipBase: ShipBase{ + conn: conn, + transport: transport, + mine: true, + secure: false, + // Calculate KCP's MSS. + loadSize: kcp.IKCP_MTU_DEF - kcp.IKCP_OVERHEAD, + }, + } + + ship.initBase() + return ship, nil +} + +func establishKCPPier(transport *hub.Transport, dockingRequests chan *DockingRequest) (Pier, error) { + listener, err := kcp.Listen(net.JoinHostPort("", portToA(transport.Port))) + if err != nil { + return nil, err + } + + pier := &KCPPier{ + PierBase: PierBase{ + transport: transport, + listener: listener, + dockingRequests: dockingRequests, + }, + } + pier.PierBase.dockShip = pier.dockShip + pier.initBase() + return pier, nil +} + +func (pier *KCPPier) dockShip() (Ship, error) { + conn, err := pier.listener.Accept() + if err != nil { + return nil, err + } + + ship := &KCPShip{ + ShipBase: ShipBase{ + conn: conn, + transport: pier.transport, + mine: false, + secure: false, + // Calculate KCP's MSS. + loadSize: kcp.IKCP_MTU_DEF - kcp.IKCP_OVERHEAD, + }, + } + + ship.initBase() + return ship, nil +} +*/ diff --git a/spn/ships/launch.go b/spn/ships/launch.go new file mode 100644 index 00000000..45a77834 --- /dev/null +++ b/spn/ships/launch.go @@ -0,0 +1,114 @@ +package ships + +import ( + "context" + "fmt" + "net" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/spn/hub" +) + +// Launch launches a new ship to the given Hub. +func Launch(ctx context.Context, h *hub.Hub, transport *hub.Transport, ip net.IP) (Ship, error) { + var transports []*hub.Transport + var ips []net.IP + + // choose transports + if transport != nil { + transports = []*hub.Transport{transport} + } else { + if h.Info == nil { + return nil, hub.ErrMissingInfo + } + transports = h.Info.ParsedTransports() + // If there are no transports, check if they were parsed. + if len(transports) == 0 && len(h.Info.Transports) > 0 { + log.Errorf("ships: %s has no parsed transports, but transports are %v", h, h.Info.Transports) + // Attempt to parse transports now. + transports, _ = hub.ParseTransports(h.Info.Transports) + } + // Fail if there are not transports. + if len(transports) == 0 { + return nil, hub.ErrMissingTransports + } + } + + // choose IPs + if ip != nil { + ips = []net.IP{ip} + } else { + if h.Info == nil { + return nil, hub.ErrMissingInfo + } + ips = make([]net.IP, 0, 3) + // If IPs have been verified, check if we can use a virtual network address. + var vnetForced bool + if h.VerifiedIPs { + vnet := GetVirtualNetworkConfig() + if vnet != nil { + virtIP := vnet.Mapping[h.ID] + if virtIP != nil { + ips = append(ips, virtIP) + if vnet.Force { + vnetForced = true + log.Infof("spn/ships: forcing virtual network address %s for %s", virtIP, h) + } else { + log.Infof("spn/ships: using virtual network address %s for %s", virtIP, h) + } + } + } + } + // Add Hub's IPs if no virtual address was forced. + if !vnetForced { + // prioritize IPv4 + if h.Info.IPv4 != nil { + ips = append(ips, h.Info.IPv4) + } + if h.Info.IPv6 != nil && netenv.IPv6Enabled() { + ips = append(ips, h.Info.IPv6) + } + } + if len(ips) == 0 { + return nil, hub.ErrMissingIPs + } + } + + // connect + var firstErr error + for _, ip := range ips { + for _, tr := range transports { + ship, err := connectTo(ctx, h, tr, ip) + if err == nil { + return ship, nil // return on success + } + + // Check if context is canceled. + if ctx.Err() != nil { + return nil, ctx.Err() + } + + // Save first error. + if firstErr == nil { + firstErr = err + } + } + } + + return nil, firstErr +} + +func connectTo(ctx context.Context, h *hub.Hub, transport *hub.Transport, ip net.IP) (Ship, error) { + builder := GetBuilder(transport.Protocol) + if builder == nil { + return nil, fmt.Errorf("protocol %s not supported", transport.Protocol) + } + + ship, err := builder.LaunchShip(ctx, transport, ip) + if err != nil { + return nil, fmt.Errorf("failed to connect to %s using %s (%s): %w", h, transport, ip, err) + } + + return ship, nil +} diff --git a/spn/ships/masking.go b/spn/ships/masking.go new file mode 100644 index 00000000..76d9fc37 --- /dev/null +++ b/spn/ships/masking.go @@ -0,0 +1,63 @@ +package ships + +import ( + "crypto/sha1" + "net" + + "github.com/mr-tron/base58" + "github.com/tevino/abool" +) + +var ( + maskingEnabled = abool.New() + maskingActive = abool.New() + maskingBytes []byte +) + +// EnableMasking enables masking with the given salt. +func EnableMasking(salt []byte) { + if maskingEnabled.SetToIf(false, true) { + maskingBytes = salt + maskingActive.Set() + } +} + +// MaskAddress masks the given address if masking is enabled and the ship is +// not public. +func (ship *ShipBase) MaskAddress(addr net.Addr) string { + // Return in plain if masking is not enabled or if ship is public. + if maskingActive.IsNotSet() || ship.Public() { + return addr.String() + } + + switch typedAddr := addr.(type) { + case *net.TCPAddr: + return ship.MaskIP(typedAddr.IP) + case *net.UDPAddr: + return ship.MaskIP(typedAddr.IP) + default: + return ship.Mask([]byte(addr.String())) + } +} + +// MaskIP masks the given IP if masking is enabled and the ship is not public. +func (ship *ShipBase) MaskIP(ip net.IP) string { + // Return in plain if masking is not enabled or if ship is public. + if maskingActive.IsNotSet() || ship.Public() { + return ip.String() + } + + return ship.Mask(ip) +} + +// Mask masks the given value. +func (ship *ShipBase) Mask(value []byte) string { + // Hash the IP with masking bytes. + hasher := sha1.New() //nolint:gosec // Not used for cryptography. + hasher.Write(maskingBytes) + hasher.Write(value) + masked := hasher.Sum(nil) + + // Return first 8 characters from the base58-encoded hash. + return "masked:" + base58.Encode(masked)[:8] +} diff --git a/spn/ships/module.go b/spn/ships/module.go new file mode 100644 index 00000000..d450185e --- /dev/null +++ b/spn/ships/module.go @@ -0,0 +1,20 @@ +package ships + +import ( + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/spn/conf" +) + +var module *modules.Module + +func init() { + module = modules.Register("ships", start, nil, nil, "cabin") +} + +func start() error { + if conf.PublicHub() { + initPageInput() + } + + return nil +} diff --git a/spn/ships/mtu.go b/spn/ships/mtu.go new file mode 100644 index 00000000..07bb1a14 --- /dev/null +++ b/spn/ships/mtu.go @@ -0,0 +1,47 @@ +package ships + +import "net" + +// MTU Calculation Configuration. +const ( + BaseMTU = 1460 // 1500 with 40 bytes extra space for special cases. + IPv4HeaderMTUSize = 20 // Without options, as not common. + IPv6HeaderMTUSize = 40 // Without options, as not common. + TCPHeaderMTUSize = 60 // Maximum size with options. + UDPHeaderMTUSize = 8 // Has no options. +) + +func (ship *ShipBase) calculateLoadSize(ip net.IP, addr net.Addr, subtract ...int) { + ship.loadSize = BaseMTU + + // Convert addr to IP if needed. + if ip == nil && addr != nil { + switch v := addr.(type) { + case *net.TCPAddr: + ip = v.IP + case *net.UDPAddr: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + } + + // Subtract IP Header, if IP is available. + if ip != nil { + if ip4 := ip.To4(); ip4 != nil { + ship.loadSize -= IPv4HeaderMTUSize + } else { + ship.loadSize -= IPv6HeaderMTUSize + } + } + + // Subtract others. + for sub := range subtract { + ship.loadSize -= sub + } + + // Raise buf size to at least load size. + if ship.bufSize < ship.loadSize { + ship.bufSize = ship.loadSize + } +} diff --git a/spn/ships/pier.go b/spn/ships/pier.go new file mode 100644 index 00000000..78483bf4 --- /dev/null +++ b/spn/ships/pier.go @@ -0,0 +1,82 @@ +package ships + +import ( + "fmt" + "net" + + "github.com/tevino/abool" + + "github.com/safing/portmaster/spn/hub" +) + +// Pier represents a network connection listener. +type Pier interface { + // String returns a human readable informational summary about the ship. + String() string + + // Transport returns the transport used for this ship. + Transport() *hub.Transport + + // Abolish closes the underlying listener and cleans up any related resources. + Abolish() +} + +// DockingRequest is a uniform request that Piers emit when a new ship arrives. +type DockingRequest struct { + Pier Pier + Ship Ship + Err error +} + +// EstablishPier is shorthand function to get the transport's builder and establish a pier. +func EstablishPier(transport *hub.Transport, dockingRequests chan Ship) (Pier, error) { + builder := GetBuilder(transport.Protocol) + if builder == nil { + return nil, fmt.Errorf("protocol %s not supported", transport.Protocol) + } + + pier, err := builder.EstablishPier(transport, dockingRequests) + if err != nil { + return nil, fmt.Errorf("failed to establish pier on %s: %w", transport, err) + } + + return pier, nil +} + +// PierBase implements common functions to comply with the Pier interface. +type PierBase struct { + // transport holds the transport definition of the pier. + transport *hub.Transport + // listeners holds the actual underlying listeners. + listeners []net.Listener + + // dockingRequests is used to report new connections to the higher layer. + dockingRequests chan Ship + + // abolishing specifies if the pier and listener is being closed. + abolishing *abool.AtomicBool +} + +func (pier *PierBase) initBase() { + // init + pier.abolishing = abool.New() +} + +// String returns a human readable informational summary about the ship. +func (pier *PierBase) String() string { + return fmt.Sprintf("", pier.transport) +} + +// Transport returns the transport used for this ship. +func (pier *PierBase) Transport() *hub.Transport { + return pier.transport +} + +// Abolish closes the underlying listener and cleans up any related resources. +func (pier *PierBase) Abolish() { + if pier.abolishing.SetToIf(false, true) { + for _, listener := range pier.listeners { + _ = listener.Close() + } + } +} diff --git a/spn/ships/registry.go b/spn/ships/registry.go new file mode 100644 index 00000000..5d3abba7 --- /dev/null +++ b/spn/ships/registry.go @@ -0,0 +1,55 @@ +package ships + +import ( + "context" + "net" + "strconv" + "sync" + + "github.com/safing/portmaster/spn/hub" +) + +// Builder is a factory that can build ships and piers of it's protocol. +type Builder struct { + LaunchShip func(ctx context.Context, transport *hub.Transport, ip net.IP) (Ship, error) + EstablishPier func(transport *hub.Transport, dockingRequests chan Ship) (Pier, error) +} + +var ( + registry = make(map[string]*Builder) + allProtocols []string + registryLock sync.Mutex +) + +// Register registers a new builder for a protocol. +func Register(protocol string, builder *Builder) { + registryLock.Lock() + defer registryLock.Unlock() + + registry[protocol] = builder +} + +// GetBuilder returns the builder for the given protocol, or nil if it does not exist. +func GetBuilder(protocol string) *Builder { + registryLock.Lock() + defer registryLock.Unlock() + + builder, ok := registry[protocol] + if !ok { + return nil + } + return builder +} + +// Protocols returns a slice with all registered protocol names. The return slice must not be edited. +func Protocols() []string { + registryLock.Lock() + defer registryLock.Unlock() + + return allProtocols +} + +// portToA transforms the given port into a string. +func portToA(port uint16) string { + return strconv.FormatUint(uint64(port), 10) +} diff --git a/spn/ships/ship.go b/spn/ships/ship.go new file mode 100644 index 00000000..4bb39b0e --- /dev/null +++ b/spn/ships/ship.go @@ -0,0 +1,220 @@ +package ships + +import ( + "errors" + "fmt" + "net" + + "github.com/tevino/abool" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/hub" +) + +const ( + defaultLoadSize = 4096 +) + +// ErrSunk is returned when a ship sunk, ie. the connection was lost. +var ErrSunk = errors.New("ship sunk") + +// Ship represents a network layer connection. +type Ship interface { + // String returns a human readable informational summary about the ship. + String() string + + // Transport returns the transport used for this ship. + Transport() *hub.Transport + + // IsMine returns whether the ship was launched from here. + IsMine() bool + + // IsSecure returns whether the ship provides transport security. + IsSecure() bool + + // Public returns whether the ship is marked as public. + Public() bool + + // MarkPublic marks the ship as public. + MarkPublic() + + // LoadSize returns the recommended data size that should be handed to Load(). + // This value will be most likely somehow related to the connection's MTU. + // Alternatively, using a multiple of LoadSize is also recommended. + LoadSize() int + + // Load loads data into the ship - ie. sends the data via the connection. + // Returns ErrSunk if the ship has already sunk earlier. + Load(data []byte) error + + // UnloadTo unloads data from the ship - ie. receives data from the + // connection - puts it into the buf. It returns the amount of data + // written and an optional error. + // Returns ErrSunk if the ship has already sunk earlier. + UnloadTo(buf []byte) (n int, err error) + + // LocalAddr returns the underlying local net.Addr of the connection. + LocalAddr() net.Addr + + // RemoteAddr returns the underlying remote net.Addr of the connection. + RemoteAddr() net.Addr + + // Sink closes the underlying connection and cleans up any related resources. + Sink() + + // MaskAddress masks the address, if enabled. + MaskAddress(addr net.Addr) string + // MaskIP masks an IP, if enabled. + MaskIP(ip net.IP) string + // Mask masks a value. + Mask(value []byte) string +} + +// ShipBase implements common functions to comply with the Ship interface. +type ShipBase struct { + // conn is the actual underlying connection. + conn net.Conn + // transport holds the transport definition of the ship. + transport *hub.Transport + + // mine specifies whether the ship was launched from here. + mine bool + // secure specifies whether the ship provides transport security. + secure bool + // public specifies whether the ship is public. + public *abool.AtomicBool + // bufSize specifies the size of the receive buffer. + bufSize int + // loadSize specifies the recommended data size that should be handed to Load(). + loadSize int + + // initial holds initial data from setting up the ship. + initial []byte + // sinking specifies if the connection is being closed. + sinking *abool.AtomicBool +} + +func (ship *ShipBase) initBase() { + // init + ship.sinking = abool.New() + ship.public = abool.New() + + // set default + if ship.loadSize == 0 { + ship.loadSize = defaultLoadSize + } + if ship.bufSize == 0 { + ship.bufSize = ship.loadSize + } +} + +// String returns a human readable informational summary about the ship. +func (ship *ShipBase) String() string { + if ship.mine { + return fmt.Sprintf("", ship.MaskAddress(ship.RemoteAddr()), ship.transport) + } + return fmt.Sprintf("", ship.MaskAddress(ship.RemoteAddr()), ship.transport) +} + +// Transport returns the transport used for this ship. +func (ship *ShipBase) Transport() *hub.Transport { + return ship.transport +} + +// IsMine returns whether the ship was launched from here. +func (ship *ShipBase) IsMine() bool { + return ship.mine +} + +// IsSecure returns whether the ship provides transport security. +func (ship *ShipBase) IsSecure() bool { + return ship.secure +} + +// Public returns whether the ship is marked as public. +func (ship *ShipBase) Public() bool { + return ship.public.IsSet() +} + +// MarkPublic marks the ship as public. +func (ship *ShipBase) MarkPublic() { + ship.public.Set() +} + +// LoadSize returns the recommended data size that should be handed to Load(). +// This value will be most likely somehow related to the connection's MTU. +// Alternatively, using a multiple of LoadSize is also recommended. +func (ship *ShipBase) LoadSize() int { + return ship.loadSize +} + +// Load loads data into the ship - ie. sends the data via the connection. +// Returns ErrSunk if the ship has already sunk earlier. +func (ship *ShipBase) Load(data []byte) error { + // Empty load is used as a signal to cease operaetion. + if len(data) == 0 { + if ship.sinking.SetToIf(false, true) { + _ = ship.conn.Close() + } + return nil + } + + // Send all given data. + n, err := ship.conn.Write(data) + switch { + case err != nil: + return err + case n == 0: + return errors.New("loaded 0 bytes") + case n < len(data): + // If not all data was sent, try again. + log.Debugf("spn/ships: %s only loaded %d/%d bytes", ship, n, len(data)) + data = data[n:] + return ship.Load(data) + } + + return nil +} + +// UnloadTo unloads data from the ship - ie. receives data from the +// connection - puts it into the buf. It returns the amount of data +// written and an optional error. +// Returns ErrSunk if the ship has already sunk earlier. +func (ship *ShipBase) UnloadTo(buf []byte) (n int, err error) { + // Process initial data, if there is any. + if ship.initial != nil { + // Copy as much data as possible. + copy(buf, ship.initial) + + // If buf was too small, skip the copied section. + if len(buf) < len(ship.initial) { + ship.initial = ship.initial[len(buf):] + return len(buf), nil + } + + // If everything was copied, unset the initial data. + n := len(ship.initial) + ship.initial = nil + return n, nil + } + + // Receive data. + return ship.conn.Read(buf) +} + +// LocalAddr returns the underlying local net.Addr of the connection. +func (ship *ShipBase) LocalAddr() net.Addr { + return ship.conn.LocalAddr() +} + +// RemoteAddr returns the underlying remote net.Addr of the connection. +func (ship *ShipBase) RemoteAddr() net.Addr { + return ship.conn.RemoteAddr() +} + +// Sink closes the underlying connection and cleans up any related resources. +func (ship *ShipBase) Sink() { + if ship.sinking.SetToIf(false, true) { + _ = ship.conn.Close() + } +} diff --git a/spn/ships/tcp.go b/spn/ships/tcp.go new file mode 100644 index 00000000..5ffd5b90 --- /dev/null +++ b/spn/ships/tcp.go @@ -0,0 +1,145 @@ +package ships + +import ( + "context" + "fmt" + "net" + "time" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/hub" +) + +// TCPShip is a ship that uses TCP. +type TCPShip struct { + ShipBase +} + +// TCPPier is a pier that uses TCP. +type TCPPier struct { + PierBase + + ctx context.Context + cancelCtx context.CancelFunc +} + +func init() { + Register("tcp", &Builder{ + LaunchShip: launchTCPShip, + EstablishPier: establishTCPPier, + }) +} + +func launchTCPShip(ctx context.Context, transport *hub.Transport, ip net.IP) (Ship, error) { + var dialNet string + if ip4 := ip.To4(); ip4 != nil { + dialNet = "tcp4" + } else { + dialNet = "tcp6" + } + dialer := &net.Dialer{ + Timeout: 30 * time.Second, + LocalAddr: conf.GetBindAddr(dialNet), + FallbackDelay: -1, // Disables Fast Fallback from IPv6 to IPv4. + KeepAlive: -1, // Disable keep-alive. + } + conn, err := dialer.DialContext(ctx, dialNet, net.JoinHostPort(ip.String(), portToA(transport.Port))) + if err != nil { + return nil, fmt.Errorf("failed to connect: %w", err) + } + + ship := &TCPShip{ + ShipBase: ShipBase{ + conn: conn, + transport: transport, + mine: true, + secure: false, + }, + } + + ship.calculateLoadSize(ip, nil, TCPHeaderMTUSize) + ship.initBase() + return ship, nil +} + +func establishTCPPier(transport *hub.Transport, dockingRequests chan Ship) (Pier, error) { + // Start listeners. + bindIPs := conf.GetBindIPs() + listeners := make([]net.Listener, 0, len(bindIPs)) + for _, bindIP := range bindIPs { + listener, err := net.ListenTCP("tcp", &net.TCPAddr{ + IP: bindIP, + Port: int(transport.Port), + }) + if err != nil { + return nil, fmt.Errorf("failed to listen: %w", err) + } + + listeners = append(listeners, listener) + log.Infof("spn/ships: tcp transport pier established on %s", listener.Addr()) + } + + // Create new pier. + pierCtx, cancelCtx := context.WithCancel(module.Ctx) + pier := &TCPPier{ + PierBase: PierBase{ + transport: transport, + listeners: listeners, + dockingRequests: dockingRequests, + }, + ctx: pierCtx, + cancelCtx: cancelCtx, + } + pier.initBase() + + // Start workers. + for _, listener := range pier.listeners { + serviceListener := listener + module.StartServiceWorker("accept TCP docking requests", 0, func(ctx context.Context) error { + return pier.dockingWorker(ctx, serviceListener) + }) + } + + return pier, nil +} + +func (pier *TCPPier) dockingWorker(_ context.Context, listener net.Listener) error { + for { + // Block until something happens. + conn, err := listener.Accept() + + // Check for errors. + switch { + case pier.ctx.Err() != nil: + return pier.ctx.Err() + case err != nil: + return err + } + + // Create new ship. + ship := &TCPShip{ + ShipBase: ShipBase{ + transport: pier.transport, + conn: conn, + mine: false, + secure: false, + }, + } + ship.calculateLoadSize(nil, conn.RemoteAddr(), TCPHeaderMTUSize) + ship.initBase() + + // Submit new docking request. + select { + case pier.dockingRequests <- ship: + case <-pier.ctx.Done(): + return pier.ctx.Err() + } + } +} + +// Abolish closes the underlying listener and cleans up any related resources. +func (pier *TCPPier) Abolish() { + pier.cancelCtx() + pier.PierBase.Abolish() +} diff --git a/spn/ships/testship.go b/spn/ships/testship.go new file mode 100644 index 00000000..6ec74b6e --- /dev/null +++ b/spn/ships/testship.go @@ -0,0 +1,154 @@ +package ships + +import ( + "net" + + "github.com/mr-tron/base58" + "github.com/tevino/abool" + + "github.com/safing/portmaster/spn/hub" +) + +// TestShip is a simulated ship that is used for testing higher level components. +type TestShip struct { + mine bool + secure bool + loadSize int + forward chan []byte + backward chan []byte + unloadTmp []byte + sinking *abool.AtomicBool +} + +// NewTestShip returns a new TestShip for simulation. +func NewTestShip(secure bool, loadSize int) *TestShip { + return &TestShip{ + mine: true, + secure: secure, + loadSize: loadSize, + forward: make(chan []byte, 100), + backward: make(chan []byte, 100), + sinking: abool.NewBool(false), + } +} + +// String returns a human readable informational summary about the ship. +func (ship *TestShip) String() string { + if ship.mine { + return "" + } + return "" +} + +// Transport returns the transport used for this ship. +func (ship *TestShip) Transport() *hub.Transport { + return &hub.Transport{ + Protocol: "dummy", + } +} + +// IsMine returns whether the ship was launched from here. +func (ship *TestShip) IsMine() bool { + return ship.mine +} + +// IsSecure returns whether the ship provides transport security. +func (ship *TestShip) IsSecure() bool { + return ship.secure +} + +// LoadSize returns the recommended data size that should be handed to Load(). +// This value will be most likely somehow related to the connection's MTU. +// Alternatively, using a multiple of LoadSize is also recommended. +func (ship *TestShip) LoadSize() int { + return ship.loadSize +} + +// Reverse creates a connected TestShip. This is used to simulate a connection instead of using a Pier. +func (ship *TestShip) Reverse() *TestShip { + return &TestShip{ + mine: !ship.mine, + secure: ship.secure, + loadSize: ship.loadSize, + forward: ship.backward, + backward: ship.forward, + sinking: abool.NewBool(false), + } +} + +// Load loads data into the ship - ie. sends the data via the connection. +// Returns ErrSunk if the ship has already sunk earlier. +func (ship *TestShip) Load(data []byte) error { + // Debugging: + // log.Debugf("spn/ship: loading %s", spew.Sdump(data)) + + // Check if ship is alive. + if ship.sinking.IsSet() { + return ErrSunk + } + + // Empty load is used as a signal to cease operaetion. + if len(data) == 0 { + ship.Sink() + return nil + } + + // Send all given data. + ship.forward <- data + + return nil +} + +// UnloadTo unloads data from the ship - ie. receives data from the +// connection - puts it into the buf. It returns the amount of data +// written and an optional error. +// Returns ErrSunk if the ship has already sunk earlier. +func (ship *TestShip) UnloadTo(buf []byte) (n int, err error) { + // Process unload tmp data, if there is any. + if ship.unloadTmp != nil { + // Copy as much data as possible. + copy(buf, ship.unloadTmp) + + // If buf was too small, skip the copied section. + if len(buf) < len(ship.unloadTmp) { + ship.unloadTmp = ship.unloadTmp[len(buf):] + return len(buf), nil + } + + // If everything was copied, unset the unloadTmp data. + n := len(ship.unloadTmp) + ship.unloadTmp = nil + return n, nil + } + + // Receive data. + data := <-ship.backward + if len(data) == 0 { + return 0, ErrSunk + } + + // Copy data, possibly save remainder for later. + copy(buf, data) + if len(buf) < len(data) { + ship.unloadTmp = data[len(buf):] + return len(buf), nil + } + return len(data), nil +} + +// Sink closes the underlying connection and cleans up any related resources. +func (ship *TestShip) Sink() { + if ship.sinking.SetToIf(false, true) { + close(ship.forward) + } +} + +// Dummy methods to conform to interface for testing. + +func (ship *TestShip) LocalAddr() net.Addr { return nil } //nolint:golint +func (ship *TestShip) RemoteAddr() net.Addr { return nil } //nolint:golint +func (ship *TestShip) Public() bool { return true } //nolint:golint +func (ship *TestShip) MarkPublic() {} //nolint:golint +func (ship *TestShip) MaskAddress(addr net.Addr) string { return addr.String() } //nolint:golint +func (ship *TestShip) MaskIP(ip net.IP) string { return ip.String() } //nolint:golint +func (ship *TestShip) Mask(value []byte) string { return base58.Encode(value) } //nolint:golint diff --git a/spn/ships/testship_test.go b/spn/ships/testship_test.go new file mode 100644 index 00000000..7e026b92 --- /dev/null +++ b/spn/ships/testship_test.go @@ -0,0 +1,58 @@ +package ships + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTestShip(t *testing.T) { + t.Parallel() + + tShip := NewTestShip(true, 100) + + // interface conformance test + var ship Ship = tShip + + srvShip := tShip.Reverse() + + for i := 0; i < 100; i++ { + // client send + err := ship.Load(testData) + if err != nil { + t.Fatalf("%s failed: %s", ship, err) + } + + // server recv + buf := getTestBuf() + _, err = srvShip.UnloadTo(buf) + if err != nil { + t.Fatalf("%s failed: %s", ship, err) + } + + // check data + assert.Equal(t, testData, buf, "should match") + fmt.Print(".") + + // server send + err = srvShip.Load(testData) + if err != nil { + t.Fatalf("%s failed: %s", ship, err) + } + + // client recv + buf = getTestBuf() + _, err = ship.UnloadTo(buf) + if err != nil { + t.Fatalf("%s failed: %s", ship, err) + } + + // check data + assert.Equal(t, testData, buf, "should match") + fmt.Print(".") + } + + ship.Sink() + srvShip.Sink() +} diff --git a/spn/ships/virtual_network.go b/spn/ships/virtual_network.go new file mode 100644 index 00000000..314112ef --- /dev/null +++ b/spn/ships/virtual_network.go @@ -0,0 +1,43 @@ +package ships + +import ( + "net" + "sync" + + "github.com/safing/portmaster/spn/hub" +) + +var ( + virtNetLock sync.Mutex + virtNetConfig *hub.VirtualNetworkConfig +) + +// SetVirtualNetworkConfig sets the virtual networking config. +func SetVirtualNetworkConfig(config *hub.VirtualNetworkConfig) { + virtNetLock.Lock() + defer virtNetLock.Unlock() + + virtNetConfig = config +} + +// GetVirtualNetworkConfig returns the virtual networking config. +func GetVirtualNetworkConfig() *hub.VirtualNetworkConfig { + virtNetLock.Lock() + defer virtNetLock.Unlock() + + return virtNetConfig +} + +// GetVirtualNetworkAddress returns the virtual network IP for the given Hub. +func GetVirtualNetworkAddress(dstHubID string) net.IP { + virtNetLock.Lock() + defer virtNetLock.Unlock() + + // Check if we have a virtual network config. + if virtNetConfig == nil { + return nil + } + + // Return mapping for given Hub ID. + return virtNetConfig.Mapping[dstHubID] +} diff --git a/spn/sluice/module.go b/spn/sluice/module.go new file mode 100644 index 00000000..63f1d2e0 --- /dev/null +++ b/spn/sluice/module.go @@ -0,0 +1,46 @@ +package sluice + +import ( + "github.com/safing/portbase/log" + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/service/netenv" + "github.com/safing/portmaster/spn/conf" +) + +var ( + module *modules.Module + + entrypointInfoMsg = []byte("You have reached the local SPN entry port, but your connection could not be matched to an SPN tunnel.\n") + + // EnableListener indicates if it should start the sluice listeners. Must be set at startup. + EnableListener bool = true +) + +func init() { + module = modules.Register("sluice", nil, start, stop, "terminal") +} + +func start() error { + // TODO: + // Listening on all interfaces for now, as we need this for Windows. + // Handle similarly to the nameserver listener. + + if conf.Client() && EnableListener { + StartSluice("tcp4", "0.0.0.0:717") + StartSluice("udp4", "0.0.0.0:717") + + if netenv.IPv6Enabled() { + StartSluice("tcp6", "[::]:717") + StartSluice("udp6", "[::]:717") + } else { + log.Warningf("spn/sluice: no IPv6 stack detected, disabling IPv6 SPN entry endpoints") + } + } + + return nil +} + +func stop() error { + stopAllSluices() + return nil +} diff --git a/spn/sluice/packet_listener.go b/spn/sluice/packet_listener.go new file mode 100644 index 00000000..3eb64cbb --- /dev/null +++ b/spn/sluice/packet_listener.go @@ -0,0 +1,277 @@ +package sluice + +import ( + "context" + "io" + "net" + "sync" + "sync/atomic" + "time" + + "github.com/tevino/abool" +) + +// PacketListener is a listener for packet based protocols. +type PacketListener struct { + sock net.PacketConn + closed *abool.AtomicBool + newConns chan *PacketConn + + lock sync.Mutex + conns map[string]*PacketConn + err error +} + +// ListenPacket creates a packet listener. +func ListenPacket(network, address string) (net.Listener, error) { + // Create a new listening packet socket. + sock, err := net.ListenPacket(network, address) + if err != nil { + return nil, err + } + + // Create listener and start workers. + ln := &PacketListener{ + sock: sock, + closed: abool.New(), + newConns: make(chan *PacketConn), + conns: make(map[string]*PacketConn), + } + module.StartServiceWorker("packet listener reader", 0, ln.reader) + module.StartServiceWorker("packet listener cleaner", time.Minute, ln.cleaner) + + return ln, nil +} + +// Accept waits for and returns the next connection to the listener. +func (ln *PacketListener) Accept() (net.Conn, error) { + conn := <-ln.newConns + if conn != nil { + return conn, nil + } + + // Check if there is a socket error. + ln.lock.Lock() + defer ln.lock.Unlock() + if ln.err != nil { + return nil, ln.err + } + + return nil, io.EOF +} + +// Close closes the listener. +// Any blocked Accept operations will be unblocked and return errors. +func (ln *PacketListener) Close() error { + if !ln.closed.SetToIf(false, true) { + return nil + } + + // Close all channels. + close(ln.newConns) + ln.lock.Lock() + defer ln.lock.Unlock() + for _, conn := range ln.conns { + close(conn.in) + } + + // Close socket. + return ln.sock.Close() +} + +// Addr returns the listener's network address. +func (ln *PacketListener) Addr() net.Addr { + return ln.sock.LocalAddr() +} + +func (ln *PacketListener) getConn(remoteAddr string) (conn *PacketConn, ok bool) { + ln.lock.Lock() + defer ln.lock.Unlock() + + conn, ok = ln.conns[remoteAddr] + return +} + +func (ln *PacketListener) setConn(conn *PacketConn) { + ln.lock.Lock() + defer ln.lock.Unlock() + + ln.conns[conn.addr.String()] = conn +} + +func (ln *PacketListener) reader(_ context.Context) error { + for { + // Read data from connection. + buf := make([]byte, 512) + n, addr, err := ln.sock.ReadFrom(buf) + if err != nil { + // Set socket error. + ln.lock.Lock() + ln.err = err + ln.lock.Unlock() + // Close and return + _ = ln.Close() + return nil //nolint:nilerr + } + buf = buf[:n] + + // Get connection and supply data. + conn, ok := ln.getConn(addr.String()) + if ok { + // Ignore if conn is closed. + if conn.closed.IsSet() { + continue + } + + select { + case conn.in <- buf: + default: + } + continue + } + + // Or create a new connection. + conn = &PacketConn{ + ln: ln, + addr: addr, + closed: abool.New(), + closing: make(chan struct{}), + buf: buf, + in: make(chan []byte, 1), + inactivityCnt: new(uint32), + } + ln.setConn(conn) + ln.newConns <- conn + } +} + +func (ln *PacketListener) cleaner(ctx context.Context) error { + for { + select { + case <-time.After(1 * time.Minute): + // Check if listener has died. + if ln.closed.IsSet() { + return nil + } + // Clean connections. + ln.cleanInactiveConns(10) + + case <-ctx.Done(): + // Exit with module stop. + return nil + } + } +} + +func (ln *PacketListener) cleanInactiveConns(overInactivityCnt uint32) { + ln.lock.Lock() + defer ln.lock.Unlock() + + for k, conn := range ln.conns { + cnt := atomic.AddUint32(conn.inactivityCnt, 1) + switch { + case cnt > overInactivityCnt*2: + delete(ln.conns, k) + case cnt > overInactivityCnt: + _ = conn.Close() + } + } +} + +// PacketConn simulates a connection for a stateless protocol. +type PacketConn struct { + ln *PacketListener + addr net.Addr + closed *abool.AtomicBool + closing chan struct{} + + buf []byte + in chan []byte + + inactivityCnt *uint32 +} + +// Read reads data from the connection. +// Read can be made to time out and return an error after a fixed +// time limit; see SetDeadline and SetReadDeadline. +func (conn *PacketConn) Read(b []byte) (n int, err error) { + // Check if connection is closed. + if conn.closed.IsSet() { + return 0, io.EOF + } + + // Mark as active. + atomic.StoreUint32(conn.inactivityCnt, 0) + + // Get new buffer. + if conn.buf == nil { + select { + case conn.buf = <-conn.in: + if conn.buf == nil { + return 0, io.EOF + } + case <-conn.closing: + return 0, io.EOF + } + } + + // Serve from buffer. + copy(b, conn.buf) + if len(b) >= len(conn.buf) { + copied := len(conn.buf) + conn.buf = nil + return copied, nil + } + copied := len(b) + conn.buf = conn.buf[copied:] + return copied, nil +} + +// Write writes data to the connection. +// Write can be made to time out and return an error after a fixed +// time limit; see SetDeadline and SetWriteDeadline. +func (conn *PacketConn) Write(b []byte) (n int, err error) { + // Check if connection is closed. + if conn.closed.IsSet() { + return 0, io.EOF + } + + // Mark as active. + atomic.StoreUint32(conn.inactivityCnt, 0) + + return conn.ln.sock.WriteTo(b, conn.addr) +} + +// Close is a no-op as UDP connections share a single socket. Just stop sending +// packets without closing. +func (conn *PacketConn) Close() error { + if conn.closed.SetToIf(false, true) { + close(conn.closing) + } + return nil +} + +// LocalAddr returns the local network address. +func (conn *PacketConn) LocalAddr() net.Addr { + return conn.ln.sock.LocalAddr() +} + +// RemoteAddr returns the remote network address. +func (conn *PacketConn) RemoteAddr() net.Addr { + return conn.addr +} + +// SetDeadline is a no-op as UDP connections share a single socket. +func (conn *PacketConn) SetDeadline(t time.Time) error { + return nil +} + +// SetReadDeadline is a no-op as UDP connections share a single socket. +func (conn *PacketConn) SetReadDeadline(t time.Time) error { + return nil +} + +// SetWriteDeadline is a no-op as UDP connections share a single socket. +func (conn *PacketConn) SetWriteDeadline(t time.Time) error { + return nil +} diff --git a/spn/sluice/request.go b/spn/sluice/request.go new file mode 100644 index 00000000..2347ed35 --- /dev/null +++ b/spn/sluice/request.go @@ -0,0 +1,78 @@ +package sluice + +import ( + "errors" + "fmt" + "net" + "time" + + "github.com/safing/portmaster/service/network" + "github.com/safing/portmaster/service/network/packet" +) + +const ( + defaultSluiceTTL = 30 * time.Second +) + +var ( + // ErrUnsupported is returned when a protocol is not supported. + ErrUnsupported = errors.New("unsupported protocol") + + // ErrSluiceOffline is returned when the sluice for a network is offline. + ErrSluiceOffline = errors.New("is offline") +) + +// Request holds request data for a sluice entry. +type Request struct { + ConnInfo *network.Connection + CallbackFn RequestCallbackFunc + Expires time.Time +} + +// RequestCallbackFunc is called for taking a over handling connection that arrived at the sluice. +type RequestCallbackFunc func(connInfo *network.Connection, conn net.Conn) + +// AwaitRequest pre-registers a connection at the sluice for initializing it when it arrives. +func AwaitRequest(connInfo *network.Connection, callbackFn RequestCallbackFunc) error { + network := getNetworkFromConnInfo(connInfo) + if network == "" { + return ErrUnsupported + } + + sluice, ok := getSluice(network) + if !ok { + return fmt.Errorf("sluice for network %s %w", network, ErrSluiceOffline) + } + + return sluice.AwaitRequest(&Request{ + ConnInfo: connInfo, + CallbackFn: callbackFn, + Expires: time.Now().Add(defaultSluiceTTL), + }) +} + +func getNetworkFromConnInfo(connInfo *network.Connection) string { + var network string + + // protocol + switch connInfo.IPProtocol { //nolint:exhaustive // Looking for specific values. + case packet.TCP: + network = "tcp" + case packet.UDP: + network = "udp" + default: + return "" + } + + // IP version + switch connInfo.IPVersion { + case packet.IPv4: + network += "4" + case packet.IPv6: + network += "6" + default: + return "" + } + + return network +} diff --git a/spn/sluice/sluice.go b/spn/sluice/sluice.go new file mode 100644 index 00000000..32a33151 --- /dev/null +++ b/spn/sluice/sluice.go @@ -0,0 +1,229 @@ +package sluice + +import ( + "context" + "fmt" + "net" + "strconv" + "sync" + "time" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/service/netenv" +) + +// Sluice is a tunnel entry listener. +type Sluice struct { + network string + address string + createListener ListenerFactory + + lock sync.Mutex + listener net.Listener + pendingRequests map[string]*Request + abandoned bool +} + +// ListenerFactory defines a function to create a listener. +type ListenerFactory func(network, address string) (net.Listener, error) + +// StartSluice starts a sluice listener at the given address. +func StartSluice(network, address string) { + s := &Sluice{ + network: network, + address: address, + pendingRequests: make(map[string]*Request), + } + + switch s.network { + case "tcp4", "tcp6": + s.createListener = net.Listen + case "udp4", "udp6": + s.createListener = ListenUDP + default: + log.Errorf("spn/sluice: cannot start sluice for %s: unsupported network", network) + return + } + + // Start service worker. + module.StartServiceWorker( + fmt.Sprintf("%s sluice listener", s.network), + 10*time.Second, + s.listenHandler, + ) +} + +// AwaitRequest pre-registers a connection. +func (s *Sluice) AwaitRequest(r *Request) error { + // Set default expiry. + if r.Expires.IsZero() { + r.Expires = time.Now().Add(defaultSluiceTTL) + } + + s.lock.Lock() + defer s.lock.Unlock() + + // Check if a pending request already exists for this local address. + key := net.JoinHostPort(r.ConnInfo.LocalIP.String(), strconv.Itoa(int(r.ConnInfo.LocalPort))) + _, exists := s.pendingRequests[key] + if exists { + return fmt.Errorf("a pending request for %s already exists", key) + } + + // Add to pending requests. + s.pendingRequests[key] = r + return nil +} + +func (s *Sluice) getRequest(address string) (r *Request, ok bool) { + s.lock.Lock() + defer s.lock.Unlock() + + r, ok = s.pendingRequests[address] + if ok { + delete(s.pendingRequests, address) + } + return +} + +func (s *Sluice) init() error { + s.lock.Lock() + defer s.lock.Unlock() + s.abandoned = false + + // start listening + s.listener = nil + ln, err := s.createListener(s.network, s.address) + if err != nil { + return fmt.Errorf("failed to listen: %w", err) + } + s.listener = ln + + // Add to registry. + addSluice(s) + + return nil +} + +func (s *Sluice) abandon() { + s.lock.Lock() + defer s.lock.Unlock() + if s.abandoned { + return + } + s.abandoned = true + + // Remove from registry. + removeSluice(s.network) + + // Close listener. + if s.listener != nil { + _ = s.listener.Close() + } + + // Notify pending requests. + for i, r := range s.pendingRequests { + r.CallbackFn(r.ConnInfo, nil) + delete(s.pendingRequests, i) + } +} + +func (s *Sluice) handleConnection(conn net.Conn) { + // Close the connection if handling is not successful. + success := false + defer func() { + if !success { + _ = conn.Close() + } + }() + + // Get IP address. + var remoteIP net.IP + switch typedAddr := conn.RemoteAddr().(type) { + case *net.TCPAddr: + remoteIP = typedAddr.IP + case *net.UDPAddr: + remoteIP = typedAddr.IP + default: + log.Warningf("spn/sluice: cannot handle connection for unsupported network %s", conn.RemoteAddr().Network()) + return + } + + // Check if the request is local. + local, err := netenv.IsMyIP(remoteIP) + if err != nil { + log.Warningf("spn/sluice: failed to check if request from %s is local: %s", remoteIP, err) + return + } + if !local { + log.Warningf("spn/sluice: received external request from %s, ignoring", remoteIP) + + // TODO: + // Do not allow this to be spammed. + // Only allow one trigger per second. + // Do not trigger by same "remote IP" in a row. + netenv.TriggerNetworkChangeCheck() + + return + } + + // Get waiting request. + r, ok := s.getRequest(conn.RemoteAddr().String()) + if !ok { + _, err := conn.Write(entrypointInfoMsg) + if err != nil { + log.Warningf("spn/sluice: new %s request from %s without pending request, but failed to reply with info msg: %s", s.network, conn.RemoteAddr(), err) + } else { + log.Debugf("spn/sluice: new %s request from %s without pending request, replied with info msg", s.network, conn.RemoteAddr()) + } + return + } + + // Hand over to callback. + log.Tracef( + "spn/sluice: new %s request from %s for %s (%s:%d)", + s.network, conn.RemoteAddr(), + r.ConnInfo.Entity.Domain, r.ConnInfo.Entity.IP, r.ConnInfo.Entity.Port, + ) + r.CallbackFn(r.ConnInfo, conn) + success = true +} + +func (s *Sluice) listenHandler(_ context.Context) error { + defer s.abandon() + err := s.init() + if err != nil { + return err + } + + // Handle new connections. + log.Infof("spn/sluice: started listening for %s requests on %s", s.network, s.listener.Addr()) + for { + conn, err := s.listener.Accept() + if err != nil { + if module.IsStopping() { + return nil + } + return fmt.Errorf("failed to accept connection: %w", err) + } + + // Handle accepted connection. + s.handleConnection(conn) + + // Clean up old leftovers. + s.cleanConnections() + } +} + +func (s *Sluice) cleanConnections() { + s.lock.Lock() + defer s.lock.Unlock() + + now := time.Now() + for address, request := range s.pendingRequests { + if now.After(request.Expires) { + delete(s.pendingRequests, address) + log.Debugf("spn/sluice: removed expired pending %s connection %s", s.network, request.ConnInfo) + } + } +} diff --git a/spn/sluice/sluices.go b/spn/sluice/sluices.go new file mode 100644 index 00000000..1ae58777 --- /dev/null +++ b/spn/sluice/sluices.go @@ -0,0 +1,47 @@ +package sluice + +import "sync" + +var ( + sluices = make(map[string]*Sluice) + sluicesLock sync.RWMutex +) + +func getSluice(network string) (s *Sluice, ok bool) { + sluicesLock.RLock() + defer sluicesLock.RUnlock() + + s, ok = sluices[network] + return +} + +func addSluice(s *Sluice) { + sluicesLock.Lock() + defer sluicesLock.Unlock() + + sluices[s.network] = s +} + +func removeSluice(network string) { + sluicesLock.Lock() + defer sluicesLock.Unlock() + + delete(sluices, network) +} + +func copySluices() map[string]*Sluice { + sluicesLock.Lock() + defer sluicesLock.Unlock() + + copied := make(map[string]*Sluice, len(sluices)) + for k, v := range sluices { + copied[k] = v + } + return copied +} + +func stopAllSluices() { + for _, sluice := range copySluices() { + sluice.abandon() + } +} diff --git a/spn/sluice/udp_listener.go b/spn/sluice/udp_listener.go new file mode 100644 index 00000000..4065d520 --- /dev/null +++ b/spn/sluice/udp_listener.go @@ -0,0 +1,334 @@ +package sluice + +import ( + "context" + "io" + "net" + "runtime" + "sync" + "sync/atomic" + "time" + + "github.com/tevino/abool" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +const onWindows = runtime.GOOS == "windows" + +// UDPListener is a listener for UDP. +type UDPListener struct { + sock *net.UDPConn + closed *abool.AtomicBool + newConns chan *UDPConn + oobSize int + + lock sync.Mutex + conns map[string]*UDPConn + err error +} + +// ListenUDP creates a packet listener. +func ListenUDP(network, address string) (net.Listener, error) { + // Parse address. + udpAddr, err := net.ResolveUDPAddr(network, address) + if err != nil { + return nil, err + } + + // Determine oob data size. + oobSize := 40 // IPv6 (measured) + if udpAddr.IP.To4() != nil { + oobSize = 32 // IPv4 (measured) + } + + // Create a new listening UDP socket. + sock, err := net.ListenUDP(network, udpAddr) + if err != nil { + return nil, err + } + + // Create listener. + ln := &UDPListener{ + sock: sock, + closed: abool.New(), + newConns: make(chan *UDPConn), + oobSize: oobSize, + conns: make(map[string]*UDPConn), + } + + // Set socket options on listener. + err = ln.setSocketOptions() + if err != nil { + return nil, err + } + + // Start workers. + module.StartServiceWorker("udp listener reader", 0, ln.reader) + module.StartServiceWorker("udp listener cleaner", time.Minute, ln.cleaner) + + return ln, nil +} + +// Accept waits for and returns the next connection to the listener. +func (ln *UDPListener) Accept() (net.Conn, error) { + conn := <-ln.newConns + if conn != nil { + return conn, nil + } + + // Check if there is a socket error. + ln.lock.Lock() + defer ln.lock.Unlock() + if ln.err != nil { + return nil, ln.err + } + + return nil, io.EOF +} + +// Close closes the listener. +// Any blocked Accept operations will be unblocked and return errors. +func (ln *UDPListener) Close() error { + if !ln.closed.SetToIf(false, true) { + return nil + } + + // Close all channels. + close(ln.newConns) + ln.lock.Lock() + defer ln.lock.Unlock() + for _, conn := range ln.conns { + close(conn.in) + } + + // Close socket. + return ln.sock.Close() +} + +// Addr returns the listener's network address. +func (ln *UDPListener) Addr() net.Addr { + return ln.sock.LocalAddr() +} + +func (ln *UDPListener) getConn(remoteAddr string) (conn *UDPConn, ok bool) { + ln.lock.Lock() + defer ln.lock.Unlock() + + conn, ok = ln.conns[remoteAddr] + return +} + +func (ln *UDPListener) setConn(conn *UDPConn) { + ln.lock.Lock() + defer ln.lock.Unlock() + + ln.conns[conn.addr.String()] = conn +} + +func (ln *UDPListener) reader(_ context.Context) error { + for { + // TODO: Find good buf size. + // With a buf size of 512 we have seen this error on Windows: + // wsarecvmsg: A message sent on a datagram socket was larger than the internal message buffer or some other network limit, or the buffer used to receive a datagram into was smaller than the datagram itself. + // UDP is not (yet) heavily used, so we can go for the 1500 bytes size for now. + + // Read data from connection. + buf := make([]byte, 1500) // TODO: see comment above. + oob := make([]byte, ln.oobSize) + n, oobn, _, addr, err := ln.sock.ReadMsgUDP(buf, oob) + if err != nil { + // Set socket error. + ln.lock.Lock() + ln.err = err + ln.lock.Unlock() + // Close and return + _ = ln.Close() + return nil //nolint:nilerr + } + buf = buf[:n] + oob = oob[:oobn] + + // Get connection and supply data. + conn, ok := ln.getConn(addr.String()) + if ok { + // Ignore if conn is closed. + if conn.closed.IsSet() { + continue + } + + select { + case conn.in <- buf: + default: + } + continue + } + + // Or create a new connection. + conn = &UDPConn{ + ln: ln, + addr: addr, + oob: oob, + closed: abool.New(), + closing: make(chan struct{}), + buf: buf, + in: make(chan []byte, 1), + inactivityCnt: new(uint32), + } + ln.setConn(conn) + ln.newConns <- conn + } +} + +func (ln *UDPListener) cleaner(ctx context.Context) error { + for { + select { + case <-time.After(1 * time.Minute): + // Check if listener has died. + if ln.closed.IsSet() { + return nil + } + // Clean connections. + ln.cleanInactiveConns(10) + + case <-ctx.Done(): + // Exit with module stop. + return nil + } + } +} + +func (ln *UDPListener) cleanInactiveConns(overInactivityCnt uint32) { + ln.lock.Lock() + defer ln.lock.Unlock() + + for k, conn := range ln.conns { + cnt := atomic.AddUint32(conn.inactivityCnt, 1) + switch { + case cnt > overInactivityCnt*2: + delete(ln.conns, k) + case cnt > overInactivityCnt: + _ = conn.Close() + } + } +} + +// setUDPSocketOptions sets socket options so that the source address for +// replies is correct. +func (ln *UDPListener) setSocketOptions() error { + // Setting socket options is not supported on windows. + if onWindows { + return nil + } + + // As we might be listening on an interface that supports both IPv4 and IPv6, + // try to set the socket options on both. + // Only report an error if it fails on both. + err4 := ipv4.NewPacketConn(ln.sock).SetControlMessage(ipv4.FlagDst|ipv4.FlagInterface, true) + err6 := ipv6.NewPacketConn(ln.sock).SetControlMessage(ipv6.FlagDst|ipv6.FlagInterface, true) + if err4 != nil && err6 != nil { + return err4 + } + + return nil +} + +// UDPConn simulates a connection for a stateless protocol. +type UDPConn struct { + ln *UDPListener + addr *net.UDPAddr + oob []byte + closed *abool.AtomicBool + closing chan struct{} + + buf []byte + in chan []byte + + inactivityCnt *uint32 +} + +// Read reads data from the connection. +// Read can be made to time out and return an error after a fixed +// time limit; see SetDeadline and SetReadDeadline. +func (conn *UDPConn) Read(b []byte) (n int, err error) { + // Check if connection is closed. + if conn.closed.IsSet() { + return 0, io.EOF + } + + // Mark as active. + atomic.StoreUint32(conn.inactivityCnt, 0) + + // Get new buffer. + if conn.buf == nil { + select { + case conn.buf = <-conn.in: + if conn.buf == nil { + return 0, io.EOF + } + case <-conn.closing: + return 0, io.EOF + } + } + + // Serve from buffer. + copy(b, conn.buf) + if len(b) >= len(conn.buf) { + copied := len(conn.buf) + conn.buf = nil + return copied, nil + } + copied := len(b) + conn.buf = conn.buf[copied:] + return copied, nil +} + +// Write writes data to the connection. +// Write can be made to time out and return an error after a fixed +// time limit; see SetDeadline and SetWriteDeadline. +func (conn *UDPConn) Write(b []byte) (n int, err error) { + // Check if connection is closed. + if conn.closed.IsSet() { + return 0, io.EOF + } + + // Mark as active. + atomic.StoreUint32(conn.inactivityCnt, 0) + + n, _, err = conn.ln.sock.WriteMsgUDP(b, conn.oob, conn.addr) + return n, err +} + +// Close is a no-op as UDP connections share a single socket. Just stop sending +// packets without closing. +func (conn *UDPConn) Close() error { + if conn.closed.SetToIf(false, true) { + close(conn.closing) + } + return nil +} + +// LocalAddr returns the local network address. +func (conn *UDPConn) LocalAddr() net.Addr { + return conn.ln.sock.LocalAddr() +} + +// RemoteAddr returns the remote network address. +func (conn *UDPConn) RemoteAddr() net.Addr { + return conn.addr +} + +// SetDeadline is a no-op as UDP connections share a single socket. +func (conn *UDPConn) SetDeadline(t time.Time) error { + return nil +} + +// SetReadDeadline is a no-op as UDP connections share a single socket. +func (conn *UDPConn) SetReadDeadline(t time.Time) error { + return nil +} + +// SetWriteDeadline is a no-op as UDP connections share a single socket. +func (conn *UDPConn) SetWriteDeadline(t time.Time) error { + return nil +} diff --git a/spn/spn.go b/spn/spn.go new file mode 100644 index 00000000..569d85de --- /dev/null +++ b/spn/spn.go @@ -0,0 +1 @@ +package spn diff --git a/spn/terminal/control_flow.go b/spn/terminal/control_flow.go new file mode 100644 index 00000000..e4d15ccf --- /dev/null +++ b/spn/terminal/control_flow.go @@ -0,0 +1,454 @@ +package terminal + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/safing/portbase/formats/varint" + "github.com/safing/portbase/modules" +) + +// FlowControl defines the flow control interface. +type FlowControl interface { + Deliver(msg *Msg) *Error + Receive() <-chan *Msg + Send(msg *Msg, timeout time.Duration) *Error + ReadyToSend() <-chan struct{} + Flush(timeout time.Duration) + StartWorkers(m *modules.Module, terminalName string) + RecvQueueLen() int + SendQueueLen() int +} + +// FlowControlType represents a flow control type. +type FlowControlType uint8 + +// Flow Control Types. +const ( + FlowControlDefault FlowControlType = 0 + FlowControlDFQ FlowControlType = 1 + FlowControlNone FlowControlType = 2 + + defaultFlowControl = FlowControlDFQ +) + +// DefaultSize returns the default flow control size. +func (fct FlowControlType) DefaultSize() uint32 { + if fct == FlowControlDefault { + fct = defaultFlowControl + } + + switch fct { + case FlowControlDFQ: + return 50000 + case FlowControlNone: + return 10000 + case FlowControlDefault: + fallthrough + default: + return 0 + } +} + +// Flow Queue Configuration. +const ( + DefaultQueueSize = 50000 + MaxQueueSize = 1000000 + forceReportBelowPercent = 0.75 +) + +// DuplexFlowQueue is a duplex flow control mechanism using queues. +type DuplexFlowQueue struct { + // ti is the Terminal that is using the DFQ. + ctx context.Context + + // submitUpstream is used to submit messages to the upstream channel. + submitUpstream func(msg *Msg, timeout time.Duration) + + // sendQueue holds the messages that are waiting to be sent. + sendQueue chan *Msg + // prioMsgs holds the number of messages to send with high priority. + prioMsgs *int32 + // sendSpace indicates the amount free slots in the recvQueue on the other end. + sendSpace *int32 + // readyToSend is used to notify sending components that there is free space. + readyToSend chan struct{} + // wakeSender is used to wake a sender in case the sendSpace was zero and the + // sender is waiting for available space. + wakeSender chan struct{} + + // recvQueue holds the messages that are waiting to be processed. + recvQueue chan *Msg + // reportedSpace indicates the amount of free slots that the other end knows + // about. + reportedSpace *int32 + // spaceReportLock locks the calculation of space to report. + spaceReportLock sync.Mutex + // forceSpaceReport forces the sender to send a space report. + forceSpaceReport chan struct{} + + // flush is used to send a finish function to the handler, which will write + // all pending messages and then call the received function. + flush chan func() +} + +// NewDuplexFlowQueue returns a new duplex flow queue. +func NewDuplexFlowQueue( + ctx context.Context, + queueSize uint32, + submitUpstream func(msg *Msg, timeout time.Duration), +) *DuplexFlowQueue { + dfq := &DuplexFlowQueue{ + ctx: ctx, + submitUpstream: submitUpstream, + sendQueue: make(chan *Msg, queueSize), + prioMsgs: new(int32), + sendSpace: new(int32), + readyToSend: make(chan struct{}), + wakeSender: make(chan struct{}, 1), + recvQueue: make(chan *Msg, queueSize), + reportedSpace: new(int32), + forceSpaceReport: make(chan struct{}, 1), + flush: make(chan func()), + } + atomic.StoreInt32(dfq.sendSpace, int32(queueSize)) + atomic.StoreInt32(dfq.reportedSpace, int32(queueSize)) + + return dfq +} + +// StartWorkers starts the necessary workers to operate the flow queue. +func (dfq *DuplexFlowQueue) StartWorkers(m *modules.Module, terminalName string) { + m.StartWorker(terminalName+" flow queue", dfq.FlowHandler) +} + +// shouldReportRecvSpace returns whether the receive space should be reported. +func (dfq *DuplexFlowQueue) shouldReportRecvSpace() bool { + return atomic.LoadInt32(dfq.reportedSpace) < int32(float32(cap(dfq.recvQueue))*forceReportBelowPercent) +} + +// decrementReportedRecvSpace decreases the reported recv space by 1 and +// returns if the receive space should be reported. +func (dfq *DuplexFlowQueue) decrementReportedRecvSpace() (shouldReportRecvSpace bool) { + return atomic.AddInt32(dfq.reportedSpace, -1) < int32(float32(cap(dfq.recvQueue))*forceReportBelowPercent) +} + +// getSendSpace returns the current send space. +func (dfq *DuplexFlowQueue) getSendSpace() int32 { + return atomic.LoadInt32(dfq.sendSpace) +} + +// decrementSendSpace decreases the send space by 1 and returns it. +func (dfq *DuplexFlowQueue) decrementSendSpace() int32 { + return atomic.AddInt32(dfq.sendSpace, -1) +} + +func (dfq *DuplexFlowQueue) addToSendSpace(n int32) { + // Add new space to send space and check if it was zero. + atomic.AddInt32(dfq.sendSpace, n) + // Wake the sender in case it is waiting. + select { + case dfq.wakeSender <- struct{}{}: + default: + } +} + +// reportableRecvSpace returns how much free space can be reported to the other +// end. The returned number must be communicated to the other end and must not +// be ignored. +func (dfq *DuplexFlowQueue) reportableRecvSpace() int32 { + // Changes to the recvQueue during calculation are no problem. + // We don't want to report space twice though! + dfq.spaceReportLock.Lock() + defer dfq.spaceReportLock.Unlock() + + // Calculate reportable receive space and add it to the reported space. + reportedSpace := atomic.LoadInt32(dfq.reportedSpace) + toReport := int32(cap(dfq.recvQueue)-len(dfq.recvQueue)) - reportedSpace + + // Never report values below zero. + // This can happen, as dfq.reportedSpace is decreased after a container is + // submitted to dfq.recvQueue by dfq.Deliver(). This race condition can only + // lower the space to report, not increase it. A simple check here solved + // this problem and keeps performance high. + // Also, don't report values of 1, as the benefit is minimal and this might + // be commonly triggered due to the buffer of the force report channel. + if toReport <= 1 { + return 0 + } + + // Add space to report to dfq.reportedSpace and return it. + atomic.AddInt32(dfq.reportedSpace, toReport) + return toReport +} + +// FlowHandler handles all flow queue internals and must be started as a worker +// in the module where it is used. +func (dfq *DuplexFlowQueue) FlowHandler(_ context.Context) error { + // The upstreamSender is started by the terminal module, but is tied to the + // flow owner instead. Make sure that the flow owner's module depends on the + // terminal module so that it is shut down earlier. + + var sendSpaceDepleted bool + var flushFinished func() + + // Drain all queues when shutting down. + defer func() { + for { + select { + case msg := <-dfq.sendQueue: + msg.Finish() + case msg := <-dfq.recvQueue: + msg.Finish() + default: + return + } + } + }() + +sending: + for { + // If the send queue is depleted, wait to be woken. + if sendSpaceDepleted { + select { + case <-dfq.wakeSender: + if dfq.getSendSpace() > 0 { + sendSpaceDepleted = false + } else { + continue sending + } + + case <-dfq.forceSpaceReport: + // Forced reporting of space. + // We do not need to check if there is enough sending space, as there is + // no data included. + spaceToReport := dfq.reportableRecvSpace() + if spaceToReport > 0 { + msg := NewMsg(varint.Pack64(uint64(spaceToReport))) + dfq.submitUpstream(msg, 0) + } + continue sending + + case <-dfq.ctx.Done(): + return nil + } + } + + // Get message from send queue. + + select { + case dfq.readyToSend <- struct{}{}: + // Notify that we are ready to send. + + case msg := <-dfq.sendQueue: + // Send message from queue. + + // If nil, the queue is being shut down. + if msg == nil { + return nil + } + + // Check if we are handling a high priority message or waiting for one. + // Mark any msgs as high priority, when there is one in the pipeline. + remainingPrioMsgs := atomic.AddInt32(dfq.prioMsgs, -1) + switch { + case remainingPrioMsgs >= 0: + msg.Unit.MakeHighPriority() + case remainingPrioMsgs < -30_000: + // Prevent wrap to positive. + // Compatible with int16 or bigger. + atomic.StoreInt32(dfq.prioMsgs, 0) + } + + // Wait for processing slot. + msg.Unit.WaitForSlot() + + // Prepend available receiving space. + msg.Data.Prepend(varint.Pack64(uint64(dfq.reportableRecvSpace()))) + + // Submit for sending upstream. + dfq.submitUpstream(msg, 0) + // Decrease the send space and set flag if depleted. + if dfq.decrementSendSpace() <= 0 { + sendSpaceDepleted = true + } + + // Check if the send queue is empty now and signal flushers. + if flushFinished != nil && len(dfq.sendQueue) == 0 { + flushFinished() + flushFinished = nil + } + + case <-dfq.forceSpaceReport: + // Forced reporting of space. + // We do not need to check if there is enough sending space, as there is + // no data included. + spaceToReport := dfq.reportableRecvSpace() + if spaceToReport > 0 { + msg := NewMsg(varint.Pack64(uint64(spaceToReport))) + dfq.submitUpstream(msg, 0) + } + + case newFlushFinishedFn := <-dfq.flush: + // Signal immediately if send queue is empty. + if len(dfq.sendQueue) == 0 { + newFlushFinishedFn() + } else { + // If there already is a flush finished function, stack them. + if flushFinished != nil { + stackedFlushFinishFn := flushFinished + flushFinished = func() { + stackedFlushFinishFn() + newFlushFinishedFn() + } + } else { + flushFinished = newFlushFinishedFn + } + } + + case <-dfq.ctx.Done(): + return nil + } + } +} + +// Flush waits for all waiting data to be sent. +func (dfq *DuplexFlowQueue) Flush(timeout time.Duration) { + // Create channel and function for notifying. + wait := make(chan struct{}) + finished := func() { + close(wait) + } + // Request flush and return when stopping. + select { + case dfq.flush <- finished: + case <-dfq.ctx.Done(): + return + case <-TimedOut(timeout): + return + } + // Wait for flush to finish and return when stopping. + select { + case <-wait: + case <-dfq.ctx.Done(): + case <-TimedOut(timeout): + } +} + +var ready = make(chan struct{}) + +func init() { + close(ready) +} + +// ReadyToSend returns a channel that can be read when data can be sent. +func (dfq *DuplexFlowQueue) ReadyToSend() <-chan struct{} { + if atomic.LoadInt32(dfq.sendSpace) > 0 { + return ready + } + return dfq.readyToSend +} + +// Send adds the given container to the send queue. +func (dfq *DuplexFlowQueue) Send(msg *Msg, timeout time.Duration) *Error { + select { + case dfq.sendQueue <- msg: + if msg.Unit.IsHighPriority() { + // Reset prioMsgs to the current queue size, so that all waiting and the + // message we just added are all handled as high priority. + atomic.StoreInt32(dfq.prioMsgs, int32(len(dfq.sendQueue))) + } + return nil + + case <-TimedOut(timeout): + msg.Finish() + return ErrTimeout + + case <-dfq.ctx.Done(): + msg.Finish() + return ErrStopping + } +} + +// Receive receives a container from the recv queue. +func (dfq *DuplexFlowQueue) Receive() <-chan *Msg { + // If the reported recv space is nearing its end, force a report. + if dfq.shouldReportRecvSpace() { + select { + case dfq.forceSpaceReport <- struct{}{}: + default: + } + } + + return dfq.recvQueue +} + +// Deliver submits a container for receiving from upstream. +func (dfq *DuplexFlowQueue) Deliver(msg *Msg) *Error { + // Ignore nil containers. + if msg == nil || msg.Data == nil { + msg.Finish() + return ErrMalformedData.With("no data") + } + + // Get and add new reported space. + addSpace, err := msg.Data.GetNextN16() + if err != nil { + msg.Finish() + return ErrMalformedData.With("failed to parse reported space: %w", err) + } + if addSpace > 0 { + dfq.addToSendSpace(int32(addSpace)) + } + // Abort processing if the container only contained a space update. + if !msg.Data.HoldsData() { + msg.Finish() + return nil + } + + select { + case dfq.recvQueue <- msg: + + // If the recv queue accepted the Container, decrement the recv space. + shouldReportRecvSpace := dfq.decrementReportedRecvSpace() + // If the reported recv space is nearing its end, force a report, if the + // sender worker is idle. + if shouldReportRecvSpace { + select { + case dfq.forceSpaceReport <- struct{}{}: + default: + } + } + + return nil + default: + // If the recv queue is full, return an error. + // The whole point of the flow queue is to guarantee that this never happens. + msg.Finish() + return ErrQueueOverflow + } +} + +// FlowStats returns a k=v formatted string of internal stats. +func (dfq *DuplexFlowQueue) FlowStats() string { + return fmt.Sprintf( + "sq=%d rq=%d sends=%d reps=%d", + len(dfq.sendQueue), + len(dfq.recvQueue), + atomic.LoadInt32(dfq.sendSpace), + atomic.LoadInt32(dfq.reportedSpace), + ) +} + +// RecvQueueLen returns the current length of the receive queue. +func (dfq *DuplexFlowQueue) RecvQueueLen() int { + return len(dfq.recvQueue) +} + +// SendQueueLen returns the current length of the send queue. +func (dfq *DuplexFlowQueue) SendQueueLen() int { + return len(dfq.sendQueue) +} diff --git a/spn/terminal/defaults.go b/spn/terminal/defaults.go new file mode 100644 index 00000000..57f17f47 --- /dev/null +++ b/spn/terminal/defaults.go @@ -0,0 +1,36 @@ +package terminal + +const ( + // UsePriorityDataMsgs defines whether priority data messages should be used. + UsePriorityDataMsgs = true +) + +// DefaultCraneControllerOpts returns the default terminal options for a crane +// controller terminal. +func DefaultCraneControllerOpts() *TerminalOpts { + return &TerminalOpts{ + Padding: 0, // Crane already applies padding. + FlowControl: FlowControlNone, + UsePriorityDataMsgs: UsePriorityDataMsgs, + } +} + +// DefaultHomeHubTerminalOpts returns the default terminal options for a crane +// terminal used for the home hub. +func DefaultHomeHubTerminalOpts() *TerminalOpts { + return &TerminalOpts{ + Padding: 0, // Crane already applies padding. + FlowControl: FlowControlDFQ, + UsePriorityDataMsgs: UsePriorityDataMsgs, + } +} + +// DefaultExpansionTerminalOpts returns the default terminal options for an +// expansion terminal. +func DefaultExpansionTerminalOpts() *TerminalOpts { + return &TerminalOpts{ + Padding: 8, + FlowControl: FlowControlDFQ, + UsePriorityDataMsgs: UsePriorityDataMsgs, + } +} diff --git a/spn/terminal/errors.go b/spn/terminal/errors.go new file mode 100644 index 00000000..619bf181 --- /dev/null +++ b/spn/terminal/errors.go @@ -0,0 +1,221 @@ +package terminal + +import ( + "context" + "errors" + "fmt" + + "github.com/safing/portbase/formats/varint" +) + +// Error is a terminal error. +type Error struct { + // id holds the internal error ID. + id uint8 + // external signifies if the error was received from the outside. + external bool + // err holds the wrapped error or the default error message. + err error +} + +// ID returns the internal ID of the error. +func (e *Error) ID() uint8 { + return e.id +} + +// Error returns the human readable format of the error. +func (e *Error) Error() string { + if e.external { + return "[ext] " + e.err.Error() + } + return e.err.Error() +} + +// IsExternal returns whether the error occurred externally. +func (e *Error) IsExternal() bool { + if e == nil { + return false + } + + return e.external +} + +// Is returns whether the given error is of the same type. +func (e *Error) Is(target error) bool { + if e == nil || target == nil { + return false + } + + t, ok := target.(*Error) //nolint:errorlint // Error implementation, not usage. + if !ok { + return false + } + return e.id == t.id +} + +// Unwrap returns the wrapped error. +func (e *Error) Unwrap() error { + if e == nil || e.err == nil { + return nil + } + return e.err +} + +// With adds context and details where the error occurred. The provided +// message is appended to the error. +// A new error with the same ID is returned and must be compared with +// errors.Is(). +func (e *Error) With(format string, a ...interface{}) *Error { + // Return nil if error is nil. + if e == nil { + return nil + } + + return &Error{ + id: e.id, + err: fmt.Errorf(e.Error()+": "+format, a...), + } +} + +// Wrap adds context higher up in the call chain. The provided message is +// prepended to the error. +// A new error with the same ID is returned and must be compared with +// errors.Is(). +func (e *Error) Wrap(format string, a ...interface{}) *Error { + // Return nil if error is nil. + if e == nil { + return nil + } + + return &Error{ + id: e.id, + err: fmt.Errorf(format+": "+e.Error(), a...), + } +} + +// AsExternal creates and returns an external version of the error. +func (e *Error) AsExternal() *Error { + // Return nil if error is nil. + if e == nil { + return nil + } + + return &Error{ + id: e.id, + err: e.err, + external: true, + } +} + +// Pack returns the serialized internal error ID. The additional message is +// lost and is replaced with the default message upon parsing. +func (e *Error) Pack() []byte { + // Return nil slice if error is nil. + if e == nil { + return nil + } + + return varint.Pack8(e.id) +} + +// ParseExternalError parses an external error. +func ParseExternalError(id []byte) (*Error, error) { + // Return nil for an empty error. + if len(id) == 0 { + return ErrStopping.AsExternal(), nil + } + + parsedID, _, err := varint.Unpack8(id) + if err != nil { + return nil, fmt.Errorf("failed to unpack error ID: %w", err) + } + + return NewExternalError(parsedID), nil +} + +// NewExternalError creates an external error based on the given ID. +func NewExternalError(id uint8) *Error { + err, ok := errorRegistry[id] + if ok { + return err.AsExternal() + } + + return ErrUnknownError.AsExternal() +} + +var errorRegistry = make(map[uint8]*Error) + +func registerError(id uint8, err error) *Error { + // Check for duplicate. + _, ok := errorRegistry[id] + if ok { + panic(fmt.Sprintf("error with id %d already registered", id)) + } + + newErr := &Error{ + id: id, + err: err, + } + + errorRegistry[id] = newErr + return newErr +} + +// func (e *Error) IsSpecial() bool { +// if e == nil { +// return false +// } +// return e.id > 0 && e.id < 8 +// } + +// IsOK returns if the error represents a "OK" or success status. +func (e *Error) IsOK() bool { + return !e.IsError() +} + +// IsError returns if the error represents an erronous condition. +func (e *Error) IsError() bool { + if e == nil || e.err == nil { + return false + } + if e.id == 0 || e.id >= 8 { + return true + } + return false +} + +// Terminal Errors. +var ( + // ErrUnknownError is the default error. + ErrUnknownError = registerError(0, errors.New("unknown error")) + + // Error IDs 1-7 are reserved for special "OK" values. + + ErrStopping = registerError(2, errors.New("stopping")) + ErrExplicitAck = registerError(3, errors.New("explicit ack")) + ErrNoActivity = registerError(4, errors.New("no activity")) + + // Errors IDs 8 and up are for regular errors. + + ErrInternalError = registerError(8, errors.New("internal error")) + ErrMalformedData = registerError(9, errors.New("malformed data")) + ErrUnexpectedMsgType = registerError(10, errors.New("unexpected message type")) + ErrUnknownOperationType = registerError(11, errors.New("unknown operation type")) + ErrUnknownOperationID = registerError(12, errors.New("unknown operation id")) + ErrPermissionDenied = registerError(13, errors.New("permission denied")) + ErrIntegrity = registerError(14, errors.New("integrity violated")) + ErrInvalidOptions = registerError(15, errors.New("invalid options")) + ErrHubNotReady = registerError(16, errors.New("hub not ready")) + ErrRateLimited = registerError(24, errors.New("rate limited")) + ErrIncorrectUsage = registerError(22, errors.New("incorrect usage")) + ErrTimeout = registerError(62, errors.New("timed out")) + ErrUnsupportedVersion = registerError(93, errors.New("unsupported version")) + ErrHubUnavailable = registerError(101, errors.New("hub unavailable")) + ErrAbandonedTerminal = registerError(102, errors.New("terminal is being abandoned")) + ErrShipSunk = registerError(108, errors.New("ship sunk")) + ErrDestinationUnavailable = registerError(113, errors.New("destination unavailable")) + ErrTryAgainLater = registerError(114, errors.New("try again later")) + ErrConnectionError = registerError(121, errors.New("connection error")) + ErrQueueOverflow = registerError(122, errors.New("queue overflowed")) + ErrCanceled = registerError(125, context.Canceled) +) diff --git a/spn/terminal/fmt.go b/spn/terminal/fmt.go new file mode 100644 index 00000000..6bebe3c0 --- /dev/null +++ b/spn/terminal/fmt.go @@ -0,0 +1,27 @@ +package terminal + +import "fmt" + +// CustomTerminalIDFormatting defines an interface for terminal to define their custom ID format. +type CustomTerminalIDFormatting interface { + CustomIDFormat() string +} + +// FmtID formats the terminal ID together with the parent's ID. +func (t *TerminalBase) FmtID() string { + if t.ext != nil { + if customFormatting, ok := t.ext.(CustomTerminalIDFormatting); ok { + return customFormatting.CustomIDFormat() + } + } + + return fmtTerminalID(t.parentID, t.id) +} + +func fmtTerminalID(craneID string, terminalID uint32) string { + return fmt.Sprintf("%s#%d", craneID, terminalID) +} + +func fmtOperationID(craneID string, terminalID, operationID uint32) string { + return fmt.Sprintf("%s#%d>%d", craneID, terminalID, operationID) +} diff --git a/spn/terminal/init.go b/spn/terminal/init.go new file mode 100644 index 00000000..b9960424 --- /dev/null +++ b/spn/terminal/init.go @@ -0,0 +1,210 @@ +package terminal + +import ( + "context" + + "github.com/safing/jess" + "github.com/safing/portbase/container" + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portbase/formats/varint" + "github.com/safing/portmaster/spn/cabin" + "github.com/safing/portmaster/spn/hub" +) + +/* + +Terminal Init Message Format: + +- Version [varint] +- Data Block [bytes; not blocked] + - TerminalOpts as DSD + +*/ + +const ( + minSupportedTerminalVersion = 1 + maxSupportedTerminalVersion = 1 +) + +// TerminalOpts holds configuration for the terminal. +type TerminalOpts struct { //nolint:golint,maligned // TODO: Rename. + Version uint8 `json:"-"` + Encrypt bool `json:"e,omitempty"` + Padding uint16 `json:"p,omitempty"` + + FlowControl FlowControlType `json:"fc,omitempty"` + FlowControlSize uint32 `json:"qs,omitempty"` // Previously was "QueueSize". + + UsePriorityDataMsgs bool `json:"pr,omitempty"` +} + +// ParseTerminalOpts parses terminal options from the container and checks if +// they are valid. +func ParseTerminalOpts(c *container.Container) (*TerminalOpts, *Error) { + // Parse and check version. + version, err := c.GetNextN8() + if err != nil { + return nil, ErrMalformedData.With("failed to parse version: %w", err) + } + if version < minSupportedTerminalVersion || version > maxSupportedTerminalVersion { + return nil, ErrUnsupportedVersion.With("requested terminal version %d", version) + } + + // Parse init message. + initMsg := &TerminalOpts{} + _, err = dsd.Load(c.CompileData(), initMsg) + if err != nil { + return nil, ErrMalformedData.With("failed to parse init message: %w", err) + } + initMsg.Version = version + + // Check if options are valid. + tErr := initMsg.Check(false) + if tErr != nil { + return nil, tErr + } + + return initMsg, nil +} + +// Pack serialized the terminal options and checks if they are valid. +func (opts *TerminalOpts) Pack() (*container.Container, *Error) { + // Check if options are valid. + tErr := opts.Check(true) + if tErr != nil { + return nil, tErr + } + + // Pack init message. + optsData, err := dsd.Dump(opts, dsd.CBOR) + if err != nil { + return nil, ErrInternalError.With("failed to pack init message: %w", err) + } + + // Compile init message. + return container.New( + varint.Pack8(opts.Version), + optsData, + ), nil +} + +// Check checks if terminal options are valid. +func (opts *TerminalOpts) Check(useDefaultsForRequired bool) *Error { + // Version is required - use default when permitted. + if opts.Version == 0 && useDefaultsForRequired { + opts.Version = 1 + } + if opts.Version < minSupportedTerminalVersion || opts.Version > maxSupportedTerminalVersion { + return ErrInvalidOptions.With("unsupported terminal version %d", opts.Version) + } + + // FlowControl is optional. + switch opts.FlowControl { + case FlowControlDefault: + // Set to default flow control. + opts.FlowControl = defaultFlowControl + case FlowControlNone, FlowControlDFQ: + // Ok. + default: + return ErrInvalidOptions.With("unknown flow control type: %d", opts.FlowControl) + } + + // FlowControlSize is required as it needs to be same on both sides. + // Use default when permitted. + if opts.FlowControlSize == 0 && useDefaultsForRequired { + opts.FlowControlSize = opts.FlowControl.DefaultSize() + } + if opts.FlowControlSize <= 0 || opts.FlowControlSize > MaxQueueSize { + return ErrInvalidOptions.With("invalid flow control size of %d", opts.FlowControlSize) + } + + return nil +} + +// NewLocalBaseTerminal creates a new local terminal base for use with inheriting terminals. +func NewLocalBaseTerminal( + ctx context.Context, + id uint32, + parentID string, + remoteHub *hub.Hub, + initMsg *TerminalOpts, + upstream Upstream, +) ( + t *TerminalBase, + initData *container.Container, + err *Error, +) { + // Pack, check and add defaults to init message. + initData, err = initMsg.Pack() + if err != nil { + return nil, nil, err + } + + // Create baseline. + t, err = createTerminalBase(ctx, id, parentID, false, initMsg, upstream) + if err != nil { + return nil, nil, err + } + + // Setup encryption if enabled. + if remoteHub != nil { + initMsg.Encrypt = true + + // Select signet (public key) of remote Hub to use. + s := remoteHub.SelectSignet() + if s == nil { + return nil, nil, ErrHubNotReady.With("failed to select signet of remote hub") + } + + // Create new session. + env := jess.NewUnconfiguredEnvelope() + env.SuiteID = jess.SuiteWireV1 + env.Recipients = []*jess.Signet{s} + jession, err := env.WireCorrespondence(nil) + if err != nil { + return nil, nil, ErrIntegrity.With("failed to initialize encryption: %w", err) + } + t.jession = jession + + // Encryption is ready for sending. + close(t.encryptionReady) + } + + return t, initData, nil +} + +// NewRemoteBaseTerminal creates a new remote terminal base for use with inheriting terminals. +func NewRemoteBaseTerminal( + ctx context.Context, + id uint32, + parentID string, + identity *cabin.Identity, + initData *container.Container, + upstream Upstream, +) ( + t *TerminalBase, + initMsg *TerminalOpts, + err *Error, +) { + // Parse init message. + initMsg, err = ParseTerminalOpts(initData) + if err != nil { + return nil, nil, err + } + + // Create baseline. + t, err = createTerminalBase(ctx, id, parentID, true, initMsg, upstream) + if err != nil { + return nil, nil, err + } + + // Setup encryption if enabled. + if initMsg.Encrypt { + if identity == nil { + return nil, nil, ErrInternalError.With("missing identity for setting up incoming encryption") + } + t.identity = identity + } + + return t, initMsg, nil +} diff --git a/spn/terminal/metrics.go b/spn/terminal/metrics.go new file mode 100644 index 00000000..0da0c326 --- /dev/null +++ b/spn/terminal/metrics.go @@ -0,0 +1,117 @@ +package terminal + +import ( + "time" + + "github.com/tevino/abool" + + "github.com/safing/portbase/api" + "github.com/safing/portbase/metrics" +) + +var metricsRegistered = abool.New() + +func registerMetrics() (err error) { + // Only register metrics once. + if !metricsRegistered.SetToIf(false, true) { + return nil + } + + // Get scheduler config and calculat scaling. + schedulerConfig := getSchedulerConfig() + scaleSlotToSecondsFactor := float64(time.Second / schedulerConfig.SlotDuration) + + // Register metrics from scheduler stats. + + _, err = metrics.NewGauge( + "spn/scheduling/unit/slotpace/max", + nil, + metricFromInt(scheduler.GetMaxSlotPace, scaleSlotToSecondsFactor), + &metrics.Options{ + Name: "SPN Scheduling Max Slot Pace (scaled to per second)", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + _, err = metrics.NewGauge( + "spn/scheduling/unit/slotpace/leveled/max", + nil, + metricFromInt(scheduler.GetMaxLeveledSlotPace, scaleSlotToSecondsFactor), + &metrics.Options{ + Name: "SPN Scheduling Max Leveled Slot Pace (scaled to per second)", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + _, err = metrics.NewGauge( + "spn/scheduling/unit/slotpace/avg", + nil, + metricFromInt(scheduler.GetAvgSlotPace, scaleSlotToSecondsFactor), + &metrics.Options{ + Name: "SPN Scheduling Avg Slot Pace (scaled to per second)", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + _, err = metrics.NewGauge( + "spn/scheduling/unit/life/avg/seconds", + nil, + metricFromNanoseconds(scheduler.GetAvgUnitLife), + &metrics.Options{ + Name: "SPN Scheduling Avg Unit Life", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + _, err = metrics.NewGauge( + "spn/scheduling/unit/workslot/avg/seconds", + nil, + metricFromNanoseconds(scheduler.GetAvgWorkSlotDuration), + &metrics.Options{ + Name: "SPN Scheduling Avg Work Slot Duration", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + _, err = metrics.NewGauge( + "spn/scheduling/unit/catchupslot/avg/seconds", + nil, + metricFromNanoseconds(scheduler.GetAvgCatchUpSlotDuration), + &metrics.Options{ + Name: "SPN Scheduling Avg Catch-Up Slot Duration", + Permission: api.PermitUser, + }, + ) + if err != nil { + return err + } + + return nil +} + +func metricFromInt(fn func() int64, scaleFactor float64) func() float64 { + return func() float64 { + return float64(fn()) * scaleFactor + } +} + +func metricFromNanoseconds(fn func() int64) func() float64 { + return func() float64 { + return float64(fn()) / float64(time.Second) + } +} diff --git a/spn/terminal/module.go b/spn/terminal/module.go new file mode 100644 index 00000000..178bc08c --- /dev/null +++ b/spn/terminal/module.go @@ -0,0 +1,80 @@ +package terminal + +import ( + "flag" + "time" + + "github.com/safing/portbase/modules" + "github.com/safing/portbase/rng" + "github.com/safing/portmaster/spn/conf" + "github.com/safing/portmaster/spn/unit" +) + +var ( + module *modules.Module + rngFeeder *rng.Feeder = rng.NewFeeder() + + scheduler *unit.Scheduler + + debugUnitScheduling bool +) + +func init() { + flag.BoolVar(&debugUnitScheduling, "debug-unit-scheduling", false, "enable debug logs of the SPN unit scheduler") + + module = modules.Register("terminal", nil, start, nil, "base") +} + +func start() error { + rngFeeder = rng.NewFeeder() + + scheduler = unit.NewScheduler(getSchedulerConfig()) + if debugUnitScheduling { + // Debug unit leaks. + scheduler.StartDebugLog() + } + module.StartServiceWorker("msg unit scheduler", 0, scheduler.SlotScheduler) + + lockOpRegistry() + + return registerMetrics() +} + +var waitForever chan time.Time + +// TimedOut returns a channel that triggers when the timeout is reached. +func TimedOut(timeout time.Duration) <-chan time.Time { + if timeout == 0 { + return waitForever + } + return time.After(timeout) +} + +// StopScheduler stops the unit scheduler. +func StopScheduler() { + if scheduler != nil { + scheduler.Stop() + } +} + +func getSchedulerConfig() *unit.SchedulerConfig { + // Client Scheduler Config. + if conf.Client() { + return &unit.SchedulerConfig{ + SlotDuration: 10 * time.Millisecond, // 100 slots per second + MinSlotPace: 10, // 1000pps - Small starting pace for low end devices. + WorkSlotPercentage: 0.9, // 90% + SlotChangeRatePerStreak: 0.1, // 10% - Increase/Decrease quickly. + StatCycleDuration: 1 * time.Minute, // Match metrics report cycle. + } + } + + // Server Scheduler Config. + return &unit.SchedulerConfig{ + SlotDuration: 10 * time.Millisecond, // 100 slots per second + MinSlotPace: 100, // 10000pps - Every server should be able to handle this. + WorkSlotPercentage: 0.7, // 70% + SlotChangeRatePerStreak: 0.05, // 5% + StatCycleDuration: 1 * time.Minute, // Match metrics report cycle. + } +} diff --git a/spn/terminal/module_test.go b/spn/terminal/module_test.go new file mode 100644 index 00000000..1f07003d --- /dev/null +++ b/spn/terminal/module_test.go @@ -0,0 +1,13 @@ +package terminal + +import ( + "testing" + + "github.com/safing/portmaster/service/core/pmtesting" + "github.com/safing/portmaster/spn/conf" +) + +func TestMain(m *testing.M) { + conf.EnablePublicHub(true) + pmtesting.TestMain(m, module) +} diff --git a/spn/terminal/msg.go b/spn/terminal/msg.go new file mode 100644 index 00000000..8ca00489 --- /dev/null +++ b/spn/terminal/msg.go @@ -0,0 +1,106 @@ +package terminal + +import ( + "fmt" + "runtime" + + "github.com/safing/portbase/container" + "github.com/safing/portmaster/spn/unit" +) + +// Msg is a message within the SPN network stack. +// It includes metadata and unit scheduling. +type Msg struct { + FlowID uint32 + Type MsgType + Data *container.Container + + // Unit scheduling. + // Note: With just 100B per packet, a uint64 (the Unit ID) is enough for + // over 1800 Exabyte. No need for overflow support. + Unit *unit.Unit +} + +// NewMsg returns a new msg. +// The FlowID is unset. +// The Type is Data. +func NewMsg(data []byte) *Msg { + msg := &Msg{ + Type: MsgTypeData, + Data: container.New(data), + Unit: scheduler.NewUnit(), + } + + // Debug unit leaks. + msg.debugWithCaller(2) + + return msg +} + +// NewEmptyMsg returns a new empty msg with an initialized Unit. +// The FlowID is unset. +// The Type is Data. +// The Data is unset. +func NewEmptyMsg() *Msg { + msg := &Msg{ + Type: MsgTypeData, + Unit: scheduler.NewUnit(), + } + + // Debug unit leaks. + msg.debugWithCaller(2) + + return msg +} + +// Pack prepends the message header (Length and ID+Type) to the data. +func (msg *Msg) Pack() { + MakeMsg(msg.Data, msg.FlowID, msg.Type) +} + +// Consume adds another Message to itself. +// The given Msg is packed before adding it to the data. +// The data is moved - not copied! +// High priority mark is inherited. +func (msg *Msg) Consume(other *Msg) { + // Pack message to be added. + other.Pack() + + // Move data. + msg.Data.AppendContainer(other.Data) + + // Inherit high priority. + if other.Unit.IsHighPriority() { + msg.Unit.MakeHighPriority() + } + + // Finish other unit. + other.Finish() +} + +// Finish signals the unit scheduler that this unit has finished processing. +// Will no-op if called on a nil Msg. +func (msg *Msg) Finish() { + // Proxying is necessary, as a nil msg still panics. + if msg == nil { + return + } + msg.Unit.Finish() +} + +// Debug registers the unit for debug output with the given source. +// Additional calls on the same unit update the unit source. +// StartDebugLog() must be called before calling DebugUnit(). +func (msg *Msg) Debug() { + msg.debugWithCaller(2) +} + +func (msg *Msg) debugWithCaller(skip int) { //nolint:unparam + if !debugUnitScheduling || msg == nil { + return + } + _, file, line, ok := runtime.Caller(skip) + if ok { + scheduler.DebugUnit(msg.Unit, fmt.Sprintf("%s:%d", file, line)) + } +} diff --git a/spn/terminal/msgtypes.go b/spn/terminal/msgtypes.go new file mode 100644 index 00000000..df712618 --- /dev/null +++ b/spn/terminal/msgtypes.go @@ -0,0 +1,66 @@ +package terminal + +import ( + "github.com/safing/portbase/container" + "github.com/safing/portbase/formats/varint" +) + +/* +Terminal and Operation Message Format: + +- Length [varint] + - If Length is 0, the remainder of given data is padding. +- IDType [varint] + - Type [uses least two significant bits] + - One of Init, Data, Stop + - ID [uses all other bits] + - The ID is currently not adapted in order to make reading raw message + easier. This means that IDs are currently always a multiple of 4. +- Data [bytes; format depends on msg type] + - MsgTypeInit: + - Data [bytes] + - MsgTypeData: + - AddAvailableSpace [varint, if Flow Queue is used] + - (Encrypted) Data [bytes] + - MsgTypeStop: + - Error Code [varint] +*/ + +// MsgType is the message type for both terminals and operations. +type MsgType uint8 + +const ( + // MsgTypeInit is used to establish a new terminal or run a new operation. + MsgTypeInit MsgType = 1 + + // MsgTypeData is used to send data to a terminal or operation. + MsgTypeData MsgType = 2 + + // MsgTypePriorityData is used to send prioritized data to a terminal or operation. + MsgTypePriorityData MsgType = 0 + + // MsgTypeStop is used to abandon a terminal or end an operation, with an optional error. + MsgTypeStop MsgType = 3 +) + +// AddIDType prepends the ID and Type header to the message. +func AddIDType(c *container.Container, id uint32, msgType MsgType) { + c.Prepend(varint.Pack32(id | uint32(msgType))) +} + +// MakeMsg prepends the message header (Length and ID+Type) to the data. +func MakeMsg(c *container.Container, id uint32, msgType MsgType) { + AddIDType(c, id, msgType) + c.PrependLength() +} + +// ParseIDType parses the combined message ID and type. +func ParseIDType(c *container.Container) (id uint32, msgType MsgType, err error) { + idType, err := c.GetNextN32() + if err != nil { + return 0, 0, err + } + + msgType = MsgType(idType % 4) + return idType - uint32(msgType), msgType, nil +} diff --git a/spn/terminal/operation.go b/spn/terminal/operation.go new file mode 100644 index 00000000..100936ec --- /dev/null +++ b/spn/terminal/operation.go @@ -0,0 +1,332 @@ +package terminal + +import ( + "context" + "sync" + "sync/atomic" + "time" + + "github.com/tevino/abool" + + "github.com/safing/portbase/container" + "github.com/safing/portbase/log" + "github.com/safing/portbase/utils" +) + +// Operation is an interface for all operations. +type Operation interface { + // InitOperationBase initialize the operation with the ID and attached terminal. + // Should not be overridden by implementations. + InitOperationBase(t Terminal, opID uint32) + + // ID returns the ID of the operation. + // Should not be overridden by implementations. + ID() uint32 + + // Type returns the operation's type ID. + // Should be overridden by implementations to return correct type ID. + Type() string + + // Deliver delivers a message to the operation. + // Meant to be overridden by implementations. + Deliver(msg *Msg) *Error + + // NewMsg creates a new message from this operation. + // Should not be overridden by implementations. + NewMsg(data []byte) *Msg + + // Send sends a message to the other side. + // Should not be overridden by implementations. + Send(msg *Msg, timeout time.Duration) *Error + + // Flush sends all messages waiting in the terminal. + // Should not be overridden by implementations. + Flush(timeout time.Duration) + + // Stopped returns whether the operation has stopped. + // Should not be overridden by implementations. + Stopped() bool + + // markStopped marks the operation as stopped. + // It returns whether the stop flag was set. + markStopped() bool + + // Stop stops the operation by unregistering it from the terminal and calling HandleStop(). + // Should not be overridden by implementations. + Stop(self Operation, err *Error) + + // HandleStop gives the operation the ability to cleanly shut down. + // The returned error is the error to send to the other side. + // Should never be called directly. Call Stop() instead. + // Meant to be overridden by implementations. + HandleStop(err *Error) (errorToSend *Error) + + // Terminal returns the terminal the operation is linked to. + // Should not be overridden by implementations. + Terminal() Terminal +} + +// OperationFactory defines an operation factory. +type OperationFactory struct { + // Type is the type id of an operation. + Type string + // Requires defines the required permissions to run an operation. + Requires Permission + // Start is the function that starts a new operation. + Start OperationStarter +} + +// OperationStarter is used to initialize operations remotely. +type OperationStarter func(attachedTerminal Terminal, opID uint32, initData *container.Container) (Operation, *Error) + +var ( + opRegistry = make(map[string]*OperationFactory) + opRegistryLock sync.Mutex + opRegistryLocked = abool.New() +) + +// RegisterOpType registers a new operation type and may only be called during +// Go's init and a module's prep phase. +func RegisterOpType(factory OperationFactory) { + // Check if we can still register an operation type. + if opRegistryLocked.IsSet() { + log.Errorf("spn/terminal: failed to register operation %s: operation registry is already locked", factory.Type) + return + } + + opRegistryLock.Lock() + defer opRegistryLock.Unlock() + + // Check if the operation type was already registered. + if _, ok := opRegistry[factory.Type]; ok { + log.Errorf("spn/terminal: failed to register operation type %s: type already registered", factory.Type) + return + } + + // Save to registry. + opRegistry[factory.Type] = &factory +} + +func lockOpRegistry() { + opRegistryLocked.Set() +} + +func (t *TerminalBase) handleOperationStart(opID uint32, initData *container.Container) { + // Check if the terminal is being abandoned. + if t.Abandoning.IsSet() { + t.StopOperation(newUnknownOp(opID, ""), ErrAbandonedTerminal) + return + } + + // Extract the requested operation name. + opType, err := initData.GetNextBlock() + if err != nil { + t.StopOperation(newUnknownOp(opID, ""), ErrMalformedData.With("failed to get init data: %w", err)) + return + } + + // Get the operation factory from the registry. + factory, ok := opRegistry[string(opType)] + if !ok { + t.StopOperation(newUnknownOp(opID, ""), ErrUnknownOperationType.With(utils.SafeFirst16Bytes(opType))) + return + } + + // Check if the Terminal has the required permission to run the operation. + if !t.HasPermission(factory.Requires) { + t.StopOperation(newUnknownOp(opID, factory.Type), ErrPermissionDenied) + return + } + + // Get terminal to attach to. + attachToTerminal := t.ext + if attachToTerminal == nil { + attachToTerminal = t + } + + // Run the operation. + op, opErr := factory.Start(attachToTerminal, opID, initData) + switch { + case opErr != nil: + // Something went wrong. + t.StopOperation(newUnknownOp(opID, factory.Type), opErr) + case op == nil: + // The Operation was successful and is done already. + log.Debugf("spn/terminal: operation %s %s executed", factory.Type, fmtOperationID(t.parentID, t.id, opID)) + t.StopOperation(newUnknownOp(opID, factory.Type), nil) + default: + // The operation started successfully and requires persistence. + t.SetActiveOp(opID, op) + log.Debugf("spn/terminal: operation %s %s started", factory.Type, fmtOperationID(t.parentID, t.id, opID)) + } +} + +// StartOperation starts the given operation by assigning it an ID and sending the given operation initialization data. +func (t *TerminalBase) StartOperation(op Operation, initData *container.Container, timeout time.Duration) *Error { + // Get terminal to attach to. + attachToTerminal := t.ext + if attachToTerminal == nil { + attachToTerminal = t + } + + // Get the next operation ID and set it on the operation with the terminal. + op.InitOperationBase(attachToTerminal, atomic.AddUint32(t.nextOpID, 8)) + + // Always add operation to the active operations, as we need to receive a + // reply in any case. + t.SetActiveOp(op.ID(), op) + + log.Debugf("spn/terminal: operation %s %s started", op.Type(), fmtOperationID(t.parentID, t.id, op.ID())) + + // Add or create the operation type block. + if initData == nil { + initData = container.New() + initData.AppendAsBlock([]byte(op.Type())) + } else { + initData.PrependAsBlock([]byte(op.Type())) + } + + // Create init msg. + msg := NewEmptyMsg() + msg.FlowID = op.ID() + msg.Type = MsgTypeInit + msg.Data = initData + msg.Unit.MakeHighPriority() + + // Send init msg. + err := op.Send(msg, timeout) + if err != nil { + msg.Finish() + } + return err +} + +// Send sends data via this terminal. +// If a timeout is set, sending will fail after the given timeout passed. +func (t *TerminalBase) Send(msg *Msg, timeout time.Duration) *Error { + // Wait for processing slot. + msg.Unit.WaitForSlot() + + // Check if the send queue has available space. + select { + case t.sendQueue <- msg: + return nil + default: + } + + // Submit message to buffer, if space is available. + select { + case t.sendQueue <- msg: + return nil + case <-TimedOut(timeout): + msg.Finish() + return ErrTimeout.With("sending via terminal") + case <-t.Ctx().Done(): + msg.Finish() + return ErrStopping + } +} + +// StopOperation sends the end signal with an optional error and then deletes +// the operation from the Terminal state and calls HandleStop() on the Operation. +func (t *TerminalBase) StopOperation(op Operation, err *Error) { + // Check if the operation has already stopped. + if !op.markStopped() { + return + } + + // Log reason the Operation is ending. Override stopping error with nil. + switch { + case err == nil: + log.Debugf("spn/terminal: operation %s %s stopped", op.Type(), fmtOperationID(t.parentID, t.id, op.ID())) + case err.IsOK(), err.Is(ErrTryAgainLater), err.Is(ErrRateLimited): + log.Debugf("spn/terminal: operation %s %s stopped: %s", op.Type(), fmtOperationID(t.parentID, t.id, op.ID()), err) + default: + log.Warningf("spn/terminal: operation %s %s failed: %s", op.Type(), fmtOperationID(t.parentID, t.id, op.ID()), err) + } + + module.StartWorker("stop operation", func(_ context.Context) error { + // Call operation stop handle function for proper shutdown cleaning up. + err = op.HandleStop(err) + + // Send error to the connected Operation, if the error is internal. + if !err.IsExternal() { + if err == nil { + err = ErrStopping + } + + msg := NewMsg(err.Pack()) + msg.FlowID = op.ID() + msg.Type = MsgTypeStop + + tErr := t.Send(msg, 10*time.Second) + if tErr != nil { + msg.Finish() + log.Warningf("spn/terminal: failed to send stop msg: %s", tErr) + } + } + + // Remove operation from terminal. + t.DeleteActiveOp(op.ID()) + + return nil + }) +} + +// GetActiveOp returns the active operation with the given ID from the +// Terminal state. +func (t *TerminalBase) GetActiveOp(opID uint32) (op Operation, ok bool) { + t.lock.RLock() + defer t.lock.RUnlock() + + op, ok = t.operations[opID] + return +} + +// SetActiveOp saves an active operation to the Terminal state. +func (t *TerminalBase) SetActiveOp(opID uint32, op Operation) { + t.lock.Lock() + defer t.lock.Unlock() + + t.operations[opID] = op +} + +// DeleteActiveOp deletes an active operation from the Terminal state. +func (t *TerminalBase) DeleteActiveOp(opID uint32) { + t.lock.Lock() + defer t.lock.Unlock() + + delete(t.operations, opID) +} + +// GetActiveOpCount returns the amount of active operations. +func (t *TerminalBase) GetActiveOpCount() int { + t.lock.RLock() + defer t.lock.RUnlock() + + return len(t.operations) +} + +func newUnknownOp(id uint32, typeID string) *unknownOp { + op := &unknownOp{ + typeID: typeID, + } + op.id = id + return op +} + +type unknownOp struct { + OperationBase + typeID string +} + +func (op *unknownOp) Type() string { + if op.typeID != "" { + return op.typeID + } + return "unknown" +} + +func (op *unknownOp) Deliver(msg *Msg) *Error { + return ErrIncorrectUsage.With("unknown op shim cannot receive") +} diff --git a/spn/terminal/operation_base.go b/spn/terminal/operation_base.go new file mode 100644 index 00000000..4b588c4f --- /dev/null +++ b/spn/terminal/operation_base.go @@ -0,0 +1,185 @@ +package terminal + +import ( + "time" + + "github.com/tevino/abool" +) + +// OperationBase provides the basic operation functionality. +type OperationBase struct { + terminal Terminal + id uint32 + stopped abool.AtomicBool +} + +// InitOperationBase initialize the operation with the ID and attached terminal. +// Should not be overridden by implementations. +func (op *OperationBase) InitOperationBase(t Terminal, opID uint32) { + op.id = opID + op.terminal = t +} + +// ID returns the ID of the operation. +// Should not be overridden by implementations. +func (op *OperationBase) ID() uint32 { + return op.id +} + +// Type returns the operation's type ID. +// Should be overridden by implementations to return correct type ID. +func (op *OperationBase) Type() string { + return "unknown" +} + +// Deliver delivers a message to the operation. +// Meant to be overridden by implementations. +func (op *OperationBase) Deliver(_ *Msg) *Error { + return ErrIncorrectUsage.With("Deliver not implemented for this operation") +} + +// NewMsg creates a new message from this operation. +// Should not be overridden by implementations. +func (op *OperationBase) NewMsg(data []byte) *Msg { + msg := NewMsg(data) + msg.FlowID = op.id + msg.Type = MsgTypeData + + // Debug unit leaks. + msg.debugWithCaller(2) + + return msg +} + +// NewEmptyMsg creates a new empty message from this operation. +// Should not be overridden by implementations. +func (op *OperationBase) NewEmptyMsg() *Msg { + msg := NewEmptyMsg() + msg.FlowID = op.id + msg.Type = MsgTypeData + + // Debug unit leaks. + msg.debugWithCaller(2) + + return msg +} + +// Send sends a message to the other side. +// Should not be overridden by implementations. +func (op *OperationBase) Send(msg *Msg, timeout time.Duration) *Error { + // Add and update metadata. + msg.FlowID = op.id + if msg.Type == MsgTypeData && msg.Unit.IsHighPriority() && UsePriorityDataMsgs { + msg.Type = MsgTypePriorityData + } + + // Wait for processing slot. + msg.Unit.WaitForSlot() + + // Send message. + tErr := op.terminal.Send(msg, timeout) + if tErr != nil { + // Finish message unit on failure. + msg.Finish() + } + return tErr +} + +// Flush sends all messages waiting in the terminal. +// Meant to be overridden by implementations. +func (op *OperationBase) Flush(timeout time.Duration) { + op.terminal.Flush(timeout) +} + +// Stopped returns whether the operation has stopped. +// Should not be overridden by implementations. +func (op *OperationBase) Stopped() bool { + return op.stopped.IsSet() +} + +// markStopped marks the operation as stopped. +// It returns whether the stop flag was set. +func (op *OperationBase) markStopped() bool { + return op.stopped.SetToIf(false, true) +} + +// Stop stops the operation by unregistering it from the terminal and calling HandleStop(). +// Should not be overridden by implementations. +func (op *OperationBase) Stop(self Operation, err *Error) { + // Stop operation from terminal. + op.terminal.StopOperation(self, err) +} + +// HandleStop gives the operation the ability to cleanly shut down. +// The returned error is the error to send to the other side. +// Should never be called directly. Call Stop() instead. +// Meant to be overridden by implementations. +func (op *OperationBase) HandleStop(err *Error) (errorToSend *Error) { + return err +} + +// Terminal returns the terminal the operation is linked to. +// Should not be overridden by implementations. +func (op *OperationBase) Terminal() Terminal { + return op.terminal +} + +// OneOffOperationBase is an operation base for operations that just have one +// message and a error return. +type OneOffOperationBase struct { + OperationBase + + Result chan *Error +} + +// Init initializes the single operation base. +func (op *OneOffOperationBase) Init() { + op.Result = make(chan *Error, 1) +} + +// HandleStop gives the operation the ability to cleanly shut down. +// The returned error is the error to send to the other side. +// Should never be called directly. Call Stop() instead. +func (op *OneOffOperationBase) HandleStop(err *Error) (errorToSend *Error) { + select { + case op.Result <- err: + default: + } + return err +} + +// MessageStreamOperationBase is an operation base for receiving a message stream. +// Every received message must be finished by the implementing operation. +type MessageStreamOperationBase struct { + OperationBase + + Delivered chan *Msg + Ended chan *Error +} + +// Init initializes the operation base. +func (op *MessageStreamOperationBase) Init(deliverQueueSize int) { + op.Delivered = make(chan *Msg, deliverQueueSize) + op.Ended = make(chan *Error, 1) +} + +// Deliver delivers data to the operation. +func (op *MessageStreamOperationBase) Deliver(msg *Msg) *Error { + select { + case op.Delivered <- msg: + return nil + default: + return ErrIncorrectUsage.With("request was not waiting for data") + } +} + +// HandleStop gives the operation the ability to cleanly shut down. +// The returned error is the error to send to the other side. +// Should never be called directly. Call Stop() instead. +func (op *MessageStreamOperationBase) HandleStop(err *Error) (errorToSend *Error) { + select { + case op.Ended <- err: + default: + } + return err +} diff --git a/spn/terminal/operation_counter.go b/spn/terminal/operation_counter.go new file mode 100644 index 00000000..59d175e0 --- /dev/null +++ b/spn/terminal/operation_counter.go @@ -0,0 +1,255 @@ +package terminal + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/safing/portbase/container" + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portbase/formats/varint" + "github.com/safing/portbase/log" +) + +// CounterOpType is the type ID for the Counter Operation. +const CounterOpType string = "debug/count" + +// CounterOp sends increasing numbers on both sides. +type CounterOp struct { //nolint:maligned + OperationBase + + wg sync.WaitGroup + server bool + opts *CounterOpts + + counterLock sync.Mutex + ClientCounter uint64 + ServerCounter uint64 + Error error +} + +// CounterOpts holds the options for CounterOp. +type CounterOpts struct { + ClientCountTo uint64 + ServerCountTo uint64 + Wait time.Duration + Flush bool + + suppressWorker bool +} + +func init() { + RegisterOpType(OperationFactory{ + Type: CounterOpType, + Start: startCounterOp, + }) +} + +// NewCounterOp returns a new CounterOp. +func NewCounterOp(t Terminal, opts CounterOpts) (*CounterOp, *Error) { + // Create operation. + op := &CounterOp{ + opts: &opts, + } + op.wg.Add(1) + + // Create argument container. + data, err := dsd.Dump(op.opts, dsd.JSON) + if err != nil { + return nil, ErrInternalError.With("failed to pack options: %w", err) + } + + // Initialize operation. + tErr := t.StartOperation(op, container.New(data), 3*time.Second) + if tErr != nil { + return nil, tErr + } + + // Start worker if needed. + if op.getRemoteCounterTarget() > 0 && !op.opts.suppressWorker { + module.StartWorker("counter sender", op.CounterWorker) + } + return op, nil +} + +func startCounterOp(t Terminal, opID uint32, data *container.Container) (Operation, *Error) { + // Create operation. + op := &CounterOp{ + server: true, + } + op.InitOperationBase(t, opID) + op.wg.Add(1) + + // Parse arguments. + opts := &CounterOpts{} + _, err := dsd.Load(data.CompileData(), opts) + if err != nil { + return nil, ErrInternalError.With("failed to unpack options: %w", err) + } + op.opts = opts + + // Start worker if needed. + if op.getRemoteCounterTarget() > 0 { + module.StartWorker("counter sender", op.CounterWorker) + } + + return op, nil +} + +// Type returns the operation's type ID. +func (op *CounterOp) Type() string { + return CounterOpType +} + +func (op *CounterOp) getCounter(sending, increase bool) uint64 { + op.counterLock.Lock() + defer op.counterLock.Unlock() + + // Use server counter, when op is server or for sending, but not when both. + if op.server != sending { + if increase { + op.ServerCounter++ + } + return op.ServerCounter + } + + if increase { + op.ClientCounter++ + } + return op.ClientCounter +} + +func (op *CounterOp) getRemoteCounterTarget() uint64 { + if op.server { + return op.opts.ClientCountTo + } + return op.opts.ServerCountTo +} + +func (op *CounterOp) isDone() bool { + op.counterLock.Lock() + defer op.counterLock.Unlock() + + return op.ClientCounter >= op.opts.ClientCountTo && + op.ServerCounter >= op.opts.ServerCountTo +} + +// Deliver delivers data to the operation. +func (op *CounterOp) Deliver(msg *Msg) *Error { + defer msg.Finish() + + nextStep, err := msg.Data.GetNextN64() + if err != nil { + op.Stop(op, ErrMalformedData.With("failed to parse next number: %w", err)) + return nil + } + + // Count and compare. + counter := op.getCounter(false, true) + + // Debugging: + // if counter < 100 || + // counter < 1000 && counter%100 == 0 || + // counter < 10000 && counter%1000 == 0 || + // counter < 100000 && counter%10000 == 0 || + // counter < 1000000 && counter%100000 == 0 { + // log.Errorf("spn/terminal: counter %s>%d recvd, now at %d", op.t.FmtID(), op.id, counter) + // } + + if counter != nextStep { + log.Warningf( + "terminal: integrity of counter op violated: received %d, expected %d", + nextStep, + counter, + ) + op.Stop(op, ErrIntegrity.With("counters mismatched")) + return nil + } + + // Check if we are done. + if op.isDone() { + op.Stop(op, nil) + } + + return nil +} + +// HandleStop handles stopping the operation. +func (op *CounterOp) HandleStop(err *Error) (errorToSend *Error) { + // Check if counting finished. + if !op.isDone() { + err := fmt.Errorf( + "counter op %d: did not finish counting (%d<-%d %d->%d)", + op.id, + op.opts.ClientCountTo, op.ClientCounter, + op.ServerCounter, op.opts.ServerCountTo, + ) + op.Error = err + } + + op.wg.Done() + return err +} + +// SendCounter sends the next counter. +func (op *CounterOp) SendCounter() *Error { + if op.Stopped() { + return ErrStopping + } + + // Increase sending counter. + counter := op.getCounter(true, true) + + // Debugging: + // if counter < 100 || + // counter < 1000 && counter%100 == 0 || + // counter < 10000 && counter%1000 == 0 || + // counter < 100000 && counter%10000 == 0 || + // counter < 1000000 && counter%100000 == 0 { + // defer log.Errorf("spn/terminal: counter %s>%d sent, now at %d", op.t.FmtID(), op.id, counter) + // } + + return op.Send(op.NewMsg(varint.Pack64(counter)), 3*time.Second) +} + +// Wait waits for the Counter Op to finish. +func (op *CounterOp) Wait() { + op.wg.Wait() +} + +// CounterWorker is a worker that sends counters. +func (op *CounterOp) CounterWorker(ctx context.Context) error { + for { + // Send counter msg. + err := op.SendCounter() + switch err { + case nil: + // All good, continue. + case ErrStopping: + // Done! + return nil + default: + // Something went wrong. + err := fmt.Errorf("counter op %d: failed to send counter: %w", op.id, err) + op.Error = err + op.Stop(op, ErrInternalError.With(err.Error())) + return nil + } + + // Maybe flush message. + if op.opts.Flush { + op.terminal.Flush(1 * time.Second) + } + + // Check if we are done with sending. + if op.getCounter(true, false) >= op.getRemoteCounterTarget() { + return nil + } + + // Maybe wait a little. + if op.opts.Wait > 0 { + time.Sleep(op.opts.Wait) + } + } +} diff --git a/spn/terminal/permission.go b/spn/terminal/permission.go new file mode 100644 index 00000000..ee39e28a --- /dev/null +++ b/spn/terminal/permission.go @@ -0,0 +1,50 @@ +package terminal + +// Permission is a bit-map of granted permissions. +type Permission uint16 + +// Permissions. +const ( + NoPermission Permission = 0x0 + MayExpand Permission = 0x1 + MayConnect Permission = 0x2 + IsHubOwner Permission = 0x100 + IsHubAdvisor Permission = 0x200 + IsCraneController Permission = 0x8000 +) + +// AuthorizingTerminal is an interface for terminals that support authorization. +type AuthorizingTerminal interface { + GrantPermission(grant Permission) + HasPermission(required Permission) bool +} + +// GrantPermission grants the specified permissions to the Terminal. +func (t *TerminalBase) GrantPermission(grant Permission) { + t.lock.Lock() + defer t.lock.Unlock() + + t.permission |= grant +} + +// HasPermission returns if the Terminal has the specified permission. +func (t *TerminalBase) HasPermission(required Permission) bool { + t.lock.RLock() + defer t.lock.RUnlock() + + return t.permission.Has(required) +} + +// Has returns if the permission includes the specified permission. +func (p Permission) Has(required Permission) bool { + return p&required == required +} + +// AddPermissions combines multiple permissions. +func AddPermissions(perms ...Permission) Permission { + var all Permission + for _, p := range perms { + all |= p + } + return all +} diff --git a/spn/terminal/rate_limit.go b/spn/terminal/rate_limit.go new file mode 100644 index 00000000..162afca0 --- /dev/null +++ b/spn/terminal/rate_limit.go @@ -0,0 +1,39 @@ +package terminal + +import "time" + +// RateLimiter is a data flow rate limiter. +type RateLimiter struct { + maxBytesPerSlot uint64 + slotBytes uint64 + slotStarted time.Time +} + +// NewRateLimiter returns a new rate limiter. +// The given MBit/s are transformed to bytes, so giving a multiple of 8 is +// advised for accurate results. +func NewRateLimiter(mbits uint64) *RateLimiter { + return &RateLimiter{ + maxBytesPerSlot: (mbits / 8) * 1_000_000, + slotStarted: time.Now(), + } +} + +// Limit is given the current transferred bytes and blocks until they may be sent. +func (rl *RateLimiter) Limit(xferBytes uint64) { + // Check if we need to limit transfer if we go over to max bytes per slot. + if rl.slotBytes > rl.maxBytesPerSlot { + // Wait if we are still within the slot. + sinceSlotStart := time.Since(rl.slotStarted) + if sinceSlotStart < time.Second { + time.Sleep(time.Second - sinceSlotStart) + } + + // Reset state for next slot. + rl.slotBytes = 0 + rl.slotStarted = time.Now() + } + + // Add new bytes after checking, as first step over the limit is fully using the limit. + rl.slotBytes += xferBytes +} diff --git a/spn/terminal/session.go b/spn/terminal/session.go new file mode 100644 index 00000000..fa2d1695 --- /dev/null +++ b/spn/terminal/session.go @@ -0,0 +1,166 @@ +package terminal + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/safing/portbase/log" +) + +const ( + rateLimitMinOps = 250 + rateLimitMaxOpsPerSecond = 5 + + rateLimitMinSuspicion = 25 + rateLimitMinPermaSuspicion = rateLimitMinSuspicion * 100 + rateLimitMaxSuspicionPerSecond = 1 + + // Make this big enough to trigger suspicion limit in first blast. + concurrencyPoolSize = 30 +) + +// Session holds terminal metadata for operations. +type Session struct { + sync.RWMutex + + // Rate Limiting. + + // started holds the unix timestamp in seconds when the session was started. + // It is set when the Session is created and may be treated as a constant. + started int64 + + // opCount is the amount of operations started (and not rate limited by suspicion). + opCount atomic.Int64 + + // suspicionScore holds a score of suspicious activity. + // Every suspicious operations is counted as at least 1. + // Rate limited operations because of suspicion are also counted as 1. + suspicionScore atomic.Int64 + + concurrencyPool chan struct{} +} + +// SessionTerminal is an interface for terminals that support authorization. +type SessionTerminal interface { + GetSession() *Session +} + +// SessionAddOn can be inherited by terminals to add support for sessions. +type SessionAddOn struct { + lock sync.Mutex + + // session holds the terminal session. + session *Session +} + +// GetSession returns the terminal's session. +func (t *SessionAddOn) GetSession() *Session { + t.lock.Lock() + defer t.lock.Unlock() + + // Create session if it does not exist. + if t.session == nil { + t.session = NewSession() + } + + return t.session +} + +// NewSession returns a new session. +func NewSession() *Session { + return &Session{ + started: time.Now().Unix() - 1, // Ensure a 1 second difference to current time. + concurrencyPool: make(chan struct{}, concurrencyPoolSize), + } +} + +// RateLimitInfo returns some basic information about the status of the rate limiter. +func (s *Session) RateLimitInfo() string { + secondsActive := time.Now().Unix() - s.started + + return fmt.Sprintf( + "%do/s %ds/s %ds", + s.opCount.Load()/secondsActive, + s.suspicionScore.Load()/secondsActive, + secondsActive, + ) +} + +// RateLimit enforces a rate and suspicion limit. +func (s *Session) RateLimit() *Error { + secondsActive := time.Now().Unix() - s.started + + // Check the suspicion limit. + score := s.suspicionScore.Load() + if score > rateLimitMinSuspicion { + scorePerSecond := score / secondsActive + if scorePerSecond >= rateLimitMaxSuspicionPerSecond { + // Add current try to suspicion score. + s.suspicionScore.Add(1) + + return ErrRateLimited + } + + // Permanently rate limit if suspicion goes over the perma min limit and + // the suspicion score is greater than 80% of the operation count. + if score > rateLimitMinPermaSuspicion && + score*5 > s.opCount.Load()*4 { // Think: 80*5 == 100*4 + return ErrRateLimited + } + } + + // Check the rate limit. + count := s.opCount.Add(1) + if count > rateLimitMinOps { + opsPerSecond := count / secondsActive + if opsPerSecond >= rateLimitMaxOpsPerSecond { + return ErrRateLimited + } + } + + return nil +} + +// Suspicion Factors. +const ( + SusFactorCommon = 1 + SusFactorWeirdButOK = 5 + SusFactorQuiteUnusual = 10 + SusFactorMustBeMalicious = 100 +) + +// ReportSuspiciousActivity reports suspicious activity of the terminal. +func (s *Session) ReportSuspiciousActivity(factor int64) { + s.suspicionScore.Add(factor) +} + +// LimitConcurrency limits concurrent executions. +// If over the limit, waiting goroutines are selected randomly. +// It returns the context error if it was canceled. +func (s *Session) LimitConcurrency(ctx context.Context, f func()) error { + // Wait for place in pool. + select { + case <-ctx.Done(): + return ctx.Err() + case s.concurrencyPool <- struct{}{}: + // We added our entry to the pool, continue with execution. + } + + // Drain own spot if pool after execution. + defer func() { + select { + case <-s.concurrencyPool: + // Own entry drained. + default: + // This should never happen, but let's play safe and not deadlock when pool is empty. + log.Warningf("spn/session: failed to drain own entry from concurrency pool") + } + }() + + // Execute and return. + f() + return nil +} diff --git a/spn/terminal/session_test.go b/spn/terminal/session_test.go new file mode 100644 index 00000000..e61d1f52 --- /dev/null +++ b/spn/terminal/session_test.go @@ -0,0 +1,94 @@ +package terminal + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestRateLimit(t *testing.T) { + t.Parallel() + + var tErr *Error + s := NewSession() + + // Everything should be okay within the min limit. + for i := 0; i < rateLimitMinOps; i++ { + tErr = s.RateLimit() + if tErr != nil { + t.Error("should not rate limit within min limit") + } + } + + // Somewhere here we should rate limiting. + for i := 0; i < rateLimitMaxOpsPerSecond; i++ { + tErr = s.RateLimit() + } + assert.ErrorIs(t, tErr, ErrRateLimited, "should rate limit") +} + +func TestSuspicionLimit(t *testing.T) { + t.Parallel() + + var tErr *Error + s := NewSession() + + // Everything should be okay within the min limit. + for i := 0; i < rateLimitMinSuspicion; i++ { + tErr = s.RateLimit() + if tErr != nil { + t.Error("should not rate limit within min limit") + } + s.ReportSuspiciousActivity(SusFactorCommon) + } + + // Somewhere here we should rate limiting. + for i := 0; i < rateLimitMaxSuspicionPerSecond; i++ { + s.ReportSuspiciousActivity(SusFactorCommon) + tErr = s.RateLimit() + } + if tErr == nil { + t.Error("should rate limit") + } +} + +func TestConcurrencyLimit(t *testing.T) { + t.Parallel() + + s := NewSession() + started := time.Now() + wg := sync.WaitGroup{} + workTime := 1 * time.Millisecond + workers := concurrencyPoolSize * 10 + + // Start many workers to test concurrency. + wg.Add(workers) + for i := 0; i < workers; i++ { + workerNum := i + go func() { + defer func() { + _ = recover() + }() + _ = s.LimitConcurrency(context.Background(), func() { + time.Sleep(workTime) + wg.Done() + + // Panic sometimes. + if workerNum%concurrencyPoolSize == 0 { + panic("test") + } + }) + }() + } + + // Wait and check time needed. + wg.Wait() + if time.Since(started) < (time.Duration(workers) * workTime / concurrencyPoolSize) { + t.Errorf("workers were too quick - only took %s", time.Since(started)) + } else { + t.Logf("workers were correctly limited - took %s", time.Since(started)) + } +} diff --git a/spn/terminal/terminal.go b/spn/terminal/terminal.go new file mode 100644 index 00000000..bbccad2f --- /dev/null +++ b/spn/terminal/terminal.go @@ -0,0 +1,909 @@ +package terminal + +import ( + "context" + "sync" + "sync/atomic" + "time" + + "github.com/tevino/abool" + + "github.com/safing/jess" + "github.com/safing/portbase/container" + "github.com/safing/portbase/log" + "github.com/safing/portbase/modules" + "github.com/safing/portbase/rng" + "github.com/safing/portmaster/spn/cabin" + "github.com/safing/portmaster/spn/conf" +) + +const ( + timeoutTicks = 5 + + clientTerminalAbandonTimeout = 15 * time.Second + serverTerminalAbandonTimeout = 5 * time.Minute +) + +// Terminal represents a terminal. +type Terminal interface { //nolint:golint // Being explicit is helpful here. + // ID returns the terminal ID. + ID() uint32 + // Ctx returns the terminal context. + Ctx() context.Context + + // Deliver delivers a message to the terminal. + // Should not be overridden by implementations. + Deliver(msg *Msg) *Error + // Send is used by others to send a message through the terminal. + // Should not be overridden by implementations. + Send(msg *Msg, timeout time.Duration) *Error + // Flush sends all messages waiting in the terminal. + // Should not be overridden by implementations. + Flush(timeout time.Duration) + + // StartOperation starts the given operation by assigning it an ID and sending the given operation initialization data. + // Should not be overridden by implementations. + StartOperation(op Operation, initData *container.Container, timeout time.Duration) *Error + // StopOperation stops the given operation. + // Should not be overridden by implementations. + StopOperation(op Operation, err *Error) + + // Abandon shuts down the terminal unregistering it from upstream and calling HandleAbandon(). + // Should not be overridden by implementations. + Abandon(err *Error) + // HandleAbandon gives the terminal the ability to cleanly shut down. + // The terminal is still fully functional at this point. + // The returned error is the error to send to the other side. + // Should never be called directly. Call Abandon() instead. + // Meant to be overridden by implementations. + HandleAbandon(err *Error) (errorToSend *Error) + // HandleDestruction gives the terminal the ability to clean up. + // The terminal has already fully shut down at this point. + // Should never be called directly. Call Abandon() instead. + // Meant to be overridden by implementations. + HandleDestruction(err *Error) + + // FmtID formats the terminal ID (including parent IDs). + // May be overridden by implementations. + FmtID() string +} + +// TerminalBase contains the basic functions of a terminal. +type TerminalBase struct { //nolint:golint,maligned // Being explicit is helpful here. + // TODO: Fix maligned. + Terminal // Interface check. + + lock sync.RWMutex + + // id is the underlying id of the Terminal. + id uint32 + // parentID is the id of the parent component. + parentID string + + // ext holds the extended terminal so that the base terminal can access custom functions. + ext Terminal + // sendQueue holds message to be sent. + sendQueue chan *Msg + // flowControl holds the flow control system. + flowControl FlowControl + // upstream represents the upstream (parent) terminal. + upstream Upstream + + // deliverProxy is populated with the configured deliver function + deliverProxy func(msg *Msg) *Error + // recvProxy is populated with the configured recv function + recvProxy func() <-chan *Msg + + // ctx is the context of the Terminal. + ctx context.Context + // cancelCtx cancels ctx. + cancelCtx context.CancelFunc + + // waitForFlush signifies if sending should be delayed until the next call + // to Flush() + waitForFlush *abool.AtomicBool + // flush is used to send a finish function to the handler, which will write + // all pending messages and then call the received function. + flush chan func() + // idleTicker ticks for increasing and checking the idle counter. + idleTicker *time.Ticker + // idleCounter counts the ticks the terminal has been idle. + idleCounter *uint32 + + // jession is the jess session used for encryption. + jession *jess.Session + // jessionLock locks jession. + jessionLock sync.Mutex + // encryptionReady is set when the encryption is ready for sending messages. + encryptionReady chan struct{} + // identity is the identity used by a remote Terminal. + identity *cabin.Identity + + // operations holds references to all active operations that require persistence. + operations map[uint32]Operation + // nextOpID holds the next operation ID. + nextOpID *uint32 + // permission holds the permissions of the terminal. + permission Permission + + // opts holds the terminal options. It must not be modified after the terminal + // has started. + opts *TerminalOpts + + // lastUnknownOpID holds the operation ID of the last data message received + // for an unknown operation ID. + lastUnknownOpID uint32 + // lastUnknownOpMsgs holds the amount of continuous data messages received + // for the operation ID in lastUnknownOpID. + lastUnknownOpMsgs uint32 + + // Abandoning indicates if the Terminal is being abandoned. The main handlers + // will keep running until the context has been canceled by the abandon + // procedure. + // No new operations should be started. + // Whoever initiates the abandoning must also start the abandon procedure. + Abandoning *abool.AtomicBool +} + +func createTerminalBase( + ctx context.Context, + id uint32, + parentID string, + remote bool, + initMsg *TerminalOpts, + upstream Upstream, +) (*TerminalBase, *Error) { + t := &TerminalBase{ + id: id, + parentID: parentID, + sendQueue: make(chan *Msg), + upstream: upstream, + waitForFlush: abool.New(), + flush: make(chan func()), + idleTicker: time.NewTicker(time.Minute), + idleCounter: new(uint32), + encryptionReady: make(chan struct{}), + operations: make(map[uint32]Operation), + nextOpID: new(uint32), + opts: initMsg, + Abandoning: abool.New(), + } + // Stop ticking to disable timeout. + t.idleTicker.Stop() + // Shift next operation ID if remote. + if remote { + atomic.AddUint32(t.nextOpID, 4) + } + // Create context. + t.ctx, t.cancelCtx = context.WithCancel(ctx) + + // Create flow control. + switch initMsg.FlowControl { + case FlowControlDFQ: + t.flowControl = NewDuplexFlowQueue(t.Ctx(), initMsg.FlowControlSize, t.submitToUpstream) + t.deliverProxy = t.flowControl.Deliver + t.recvProxy = t.flowControl.Receive + case FlowControlNone: + deliver := make(chan *Msg, initMsg.FlowControlSize) + t.deliverProxy = MakeDirectDeliveryDeliverFunc(ctx, deliver) + t.recvProxy = MakeDirectDeliveryRecvFunc(deliver) + case FlowControlDefault: + fallthrough + default: + return nil, ErrInternalError.With("unknown flow control type %d", initMsg.FlowControl) + } + + return t, nil +} + +// ID returns the Terminal's ID. +func (t *TerminalBase) ID() uint32 { + return t.id +} + +// Ctx returns the Terminal's context. +func (t *TerminalBase) Ctx() context.Context { + return t.ctx +} + +// SetTerminalExtension sets the Terminal's extension. This function is not +// guarded and may only be used during initialization. +func (t *TerminalBase) SetTerminalExtension(ext Terminal) { + t.ext = ext +} + +// SetTimeout sets the Terminal's idle timeout duration. +// It is broken down into slots internally. +func (t *TerminalBase) SetTimeout(d time.Duration) { + t.idleTicker.Reset(d / timeoutTicks) +} + +// Deliver on TerminalBase only exists to conform to the interface. It must be +// overridden by an actual implementation. +func (t *TerminalBase) Deliver(msg *Msg) *Error { + // Deliver via configured proxy. + err := t.deliverProxy(msg) + if err != nil { + msg.Finish() + } + + return err +} + +// StartWorkers starts the necessary workers to operate the Terminal. +func (t *TerminalBase) StartWorkers(m *modules.Module, terminalName string) { + // Start terminal workers. + m.StartWorker(terminalName+" handler", t.Handler) + m.StartWorker(terminalName+" sender", t.Sender) + + // Start any flow control workers. + if t.flowControl != nil { + t.flowControl.StartWorkers(m, terminalName) + } +} + +const ( + sendThresholdLength = 100 // bytes + sendMaxLength = 4000 // bytes + sendThresholdMaxWait = 20 * time.Millisecond +) + +// Handler receives and handles messages and must be started as a worker in the +// module where the Terminal is used. +func (t *TerminalBase) Handler(_ context.Context) error { + defer t.Abandon(ErrInternalError.With("handler died")) + + var msg *Msg + defer msg.Finish() + + for { + select { + case <-t.ctx.Done(): + // Call Abandon just in case. + // Normally, only the StopProcedure function should cancel the context. + t.Abandon(nil) + return nil // Controlled worker exit. + + case <-t.idleTicker.C: + // If nothing happens for a while, end the session. + if atomic.AddUint32(t.idleCounter, 1) > timeoutTicks { + // Abandon the terminal and reset the counter. + t.Abandon(ErrNoActivity) + atomic.StoreUint32(t.idleCounter, 0) + } + + case msg = <-t.recvProxy(): + err := t.handleReceive(msg) + if err != nil { + t.Abandon(err.Wrap("failed to handle")) + return nil + } + + // Register activity. + atomic.StoreUint32(t.idleCounter, 0) + } + } +} + +// submit is used to send message from the terminal to upstream, including +// going through flow control, if configured. +// This function should be used to send message from the terminal to upstream. +func (t *TerminalBase) submit(msg *Msg, timeout time.Duration) { + // Submit directly if no flow control is configured. + if t.flowControl == nil { + t.submitToUpstream(msg, timeout) + return + } + + // Hand over to flow control. + err := t.flowControl.Send(msg, timeout) + if err != nil { + msg.Finish() + t.Abandon(err.Wrap("failed to submit to flow control")) + } +} + +// submitToUpstream is used to directly submit messages to upstream. +// This function should only be used by the flow control or submit function. +func (t *TerminalBase) submitToUpstream(msg *Msg, timeout time.Duration) { + // Add terminal ID as flow ID. + msg.FlowID = t.ID() + + // Debug unit leaks. + msg.debugWithCaller(2) + + // Submit to upstream. + err := t.upstream.Send(msg, timeout) + if err != nil { + msg.Finish() + t.Abandon(err.Wrap("failed to submit to upstream")) + } +} + +// Sender handles sending messages and must be started as a worker in the +// module where the Terminal is used. +func (t *TerminalBase) Sender(_ context.Context) error { + // Don't send messages, if the encryption is net yet set up. + // The server encryption session is only initialized with the first + // operative message, not on Terminal creation. + if t.opts.Encrypt { + select { + case <-t.ctx.Done(): + // Call Abandon just in case. + // Normally, the only the StopProcedure function should cancel the context. + t.Abandon(nil) + return nil // Controlled worker exit. + case <-t.encryptionReady: + } + } + + // Be sure to call Stop even in case of sudden death. + defer t.Abandon(ErrInternalError.With("sender died")) + + var msgBufferMsg *Msg + var msgBufferLen int + var msgBufferLimitReached bool + var sendMsgs bool + var sendMaxWait *time.Timer + var flushFinished func() + + // Finish any current unit when returning. + defer msgBufferMsg.Finish() + + // Only receive message when not sending the current msg buffer. + sendQueueOpMsgs := func() <-chan *Msg { + // Don't handle more messages, if the buffer is full. + if msgBufferLimitReached { + return nil + } + return t.sendQueue + } + + // Only wait for sending slot when the current msg buffer is ready to be sent. + readyToSend := func() <-chan struct{} { + switch { + case !sendMsgs: + // Wait until there is something to send. + return nil + case t.flowControl != nil: + // Let flow control decide when we are ready. + return t.flowControl.ReadyToSend() + default: + // Always ready. + return ready + } + } + + // Calculate current max wait time to send the msg buffer. + getSendMaxWait := func() <-chan time.Time { + if sendMaxWait != nil { + return sendMaxWait.C + } + return nil + } + +handling: + for { + select { + case <-t.ctx.Done(): + // Call Stop just in case. + // Normally, the only the StopProcedure function should cancel the context. + t.Abandon(nil) + return nil // Controlled worker exit. + + case <-t.idleTicker.C: + // If nothing happens for a while, end the session. + if atomic.AddUint32(t.idleCounter, 1) > timeoutTicks { + // Abandon the terminal and reset the counter. + t.Abandon(ErrNoActivity) + atomic.StoreUint32(t.idleCounter, 0) + } + + case msg := <-sendQueueOpMsgs(): + if msg == nil { + continue handling + } + + // Add unit to buffer unit, or use it as new buffer. + if msgBufferMsg != nil { + // Pack, append and finish additional message. + msgBufferMsg.Consume(msg) + } else { + // Pack operation message. + msg.Pack() + // Convert to message of terminal. + msgBufferMsg = msg + msgBufferMsg.FlowID = t.ID() + msgBufferMsg.Type = MsgTypeData + } + msgBufferLen += msg.Data.Length() + + // Check if there is enough data to hit the sending threshold. + if msgBufferLen >= sendThresholdLength { + sendMsgs = true + } else if sendMaxWait == nil && t.waitForFlush.IsNotSet() { + sendMaxWait = time.NewTimer(sendThresholdMaxWait) + } + + // Check if we have reached the maximum buffer size. + if msgBufferLen >= sendMaxLength { + msgBufferLimitReached = true + } + + // Register activity. + atomic.StoreUint32(t.idleCounter, 0) + + case <-getSendMaxWait(): + // The timer for waiting for more data has ended. + // Send all available data if not forced to wait for a flush. + if t.waitForFlush.IsNotSet() { + sendMsgs = true + } + + case newFlushFinishedFn := <-t.flush: + // We are flushing - stop waiting. + t.waitForFlush.UnSet() + + // Signal immediately if msg buffer is empty. + if msgBufferLen == 0 { + newFlushFinishedFn() + } else { + // If there already is a flush finished function, stack them. + if flushFinished != nil { + stackedFlushFinishFn := flushFinished + flushFinished = func() { + stackedFlushFinishFn() + newFlushFinishedFn() + } + } else { + flushFinished = newFlushFinishedFn + } + } + + // Force sending data now. + sendMsgs = true + + case <-readyToSend(): + // Reset sending flags. + sendMsgs = false + msgBufferLimitReached = false + + // Send if there is anything to send. + var err *Error + if msgBufferLen > 0 { + // Update message type to include priority. + if msgBufferMsg.Type == MsgTypeData && + msgBufferMsg.Unit.IsHighPriority() && + t.opts.UsePriorityDataMsgs { + msgBufferMsg.Type = MsgTypePriorityData + } + + // Wait for clearance on initial msg only. + msgBufferMsg.Unit.WaitForSlot() + + err = t.sendOpMsgs(msgBufferMsg) + } + + // Reset buffer. + msgBufferMsg = nil + msgBufferLen = 0 + + // Reset send wait timer. + if sendMaxWait != nil { + sendMaxWait.Stop() + sendMaxWait = nil + } + + // Check if we are flushing and need to notify. + if flushFinished != nil { + flushFinished() + flushFinished = nil + } + + // Handle error after state updates. + if err != nil { + t.Abandon(err.With("failed to send")) + continue handling + } + } + } +} + +// WaitForFlush makes the terminal pause all sending until the next call to +// Flush(). +func (t *TerminalBase) WaitForFlush() { + t.waitForFlush.Set() +} + +// Flush sends all data waiting to be sent. +func (t *TerminalBase) Flush(timeout time.Duration) { + // Create channel and function for notifying. + wait := make(chan struct{}) + finished := func() { + close(wait) + } + // Request flush and return when stopping. + select { + case t.flush <- finished: + case <-t.Ctx().Done(): + return + case <-TimedOut(timeout): + return + } + // Wait for flush to finish and return when stopping. + select { + case <-wait: + case <-t.Ctx().Done(): + return + case <-TimedOut(timeout): + return + } + + // Flush flow control, if configured. + if t.flowControl != nil { + t.flowControl.Flush(timeout) + } +} + +func (t *TerminalBase) encrypt(c *container.Container) (*container.Container, *Error) { + if !t.opts.Encrypt { + return c, nil + } + + t.jessionLock.Lock() + defer t.jessionLock.Unlock() + + letter, err := t.jession.Close(c.CompileData()) + if err != nil { + return nil, ErrIntegrity.With("failed to encrypt: %w", err) + } + + encryptedData, err := letter.ToWire() + if err != nil { + return nil, ErrInternalError.With("failed to pack letter: %w", err) + } + + return encryptedData, nil +} + +func (t *TerminalBase) decrypt(c *container.Container) (*container.Container, *Error) { + if !t.opts.Encrypt { + return c, nil + } + + t.jessionLock.Lock() + defer t.jessionLock.Unlock() + + letter, err := jess.LetterFromWire(c) + if err != nil { + return nil, ErrMalformedData.With("failed to parse letter: %w", err) + } + + // Setup encryption if not yet done. + if t.jession == nil { + if t.identity == nil { + return nil, ErrInternalError.With("missing identity for setting up incoming encryption") + } + + // Create jess session. + t.jession, err = letter.WireCorrespondence(t.identity) + if err != nil { + return nil, ErrIntegrity.With("failed to initialize incoming encryption: %w", err) + } + + // Don't need that anymore. + t.identity = nil + + // Encryption is ready for sending. + close(t.encryptionReady) + } + + decryptedData, err := t.jession.Open(letter) + if err != nil { + return nil, ErrIntegrity.With("failed to decrypt: %w", err) + } + + return container.New(decryptedData), nil +} + +func (t *TerminalBase) handleReceive(msg *Msg) *Error { + msg.Unit.WaitForSlot() + defer msg.Finish() + + // Debugging: + // log.Errorf("spn/terminal %s handling tmsg: %s", t.FmtID(), spew.Sdump(c.CompileData())) + + // Check if message is empty. This will be the case if a message was only + // for updated the available space of the flow queue. + if !msg.Data.HoldsData() { + return nil + } + + // Decrypt if enabled. + var tErr *Error + msg.Data, tErr = t.decrypt(msg.Data) + if tErr != nil { + return tErr + } + + // Handle operation messages. + for msg.Data.HoldsData() { + // Get next message length. + msgLength, err := msg.Data.GetNextN32() + if err != nil { + return ErrMalformedData.With("failed to get operation msg length: %w", err) + } + if msgLength == 0 { + // Remainder is padding. + // Padding can only be at the end of the segment. + t.handlePaddingMsg(msg.Data) + return nil + } + + // Get op msg data. + msgData, err := msg.Data.GetAsContainer(int(msgLength)) + if err != nil { + return ErrMalformedData.With("failed to get operation msg data (%d/%d bytes): %w", msg.Data.Length(), msgLength, err) + } + + // Handle op msg. + if handleErr := t.handleOpMsg(msgData); handleErr != nil { + return handleErr + } + } + + return nil +} + +func (t *TerminalBase) handleOpMsg(data *container.Container) *Error { + // Debugging: + // log.Errorf("spn/terminal %s handling opmsg: %s", t.FmtID(), spew.Sdump(data.CompileData())) + + // Parse message operation id, type. + opID, msgType, err := ParseIDType(data) + if err != nil { + return ErrMalformedData.With("failed to parse operation msg id/type: %w", err) + } + + switch msgType { + case MsgTypeInit: + t.handleOperationStart(opID, data) + + case MsgTypeData, MsgTypePriorityData: + op, ok := t.GetActiveOp(opID) + if ok && !op.Stopped() { + // Create message from data. + msg := NewEmptyMsg() + msg.FlowID = opID + msg.Type = msgType + msg.Data = data + if msg.Type == MsgTypePriorityData { + msg.Unit.MakeHighPriority() + } + + // Deliver message to operation. + tErr := op.Deliver(msg) + if tErr != nil { + // Also stop on "success" errors! + msg.Finish() + t.StopOperation(op, tErr) + } + return nil + } + + // If an active op is not found, this is likely just left-overs from a + // stopped or failed operation. + // log.Tracef("spn/terminal: %s received data msg for unknown op %d", fmtTerminalID(t.parentID, t.id), opID) + + // Send a stop error if this happens too often. + if opID == t.lastUnknownOpID { + // OpID is the same as last time. + t.lastUnknownOpMsgs++ + + // Log an warning (via StopOperation) and send a stop message every thousand. + if t.lastUnknownOpMsgs%1000 == 0 { + t.StopOperation(newUnknownOp(opID, ""), ErrUnknownOperationID.With("received %d unsolicited data msgs", t.lastUnknownOpMsgs)) + } + + // TODO: Abandon terminal at over 10000? + } else { + // OpID changed, set new ID and reset counter. + t.lastUnknownOpID = opID + t.lastUnknownOpMsgs = 1 + } + + case MsgTypeStop: + // Parse received error. + opErr, parseErr := ParseExternalError(data.CompileData()) + if parseErr != nil { + log.Warningf("spn/terminal: %s failed to parse stop error: %s", fmtTerminalID(t.parentID, t.id), parseErr) + opErr = ErrUnknownError.AsExternal() + } + + // End operation. + op, ok := t.GetActiveOp(opID) + if ok { + t.StopOperation(op, opErr) + } else { + log.Tracef("spn/terminal: %s received stop msg for unknown op %d", fmtTerminalID(t.parentID, t.id), opID) + } + + default: + log.Warningf("spn/terminal: %s received unexpected message type: %d", t.FmtID(), msgType) + return ErrUnexpectedMsgType + } + + return nil +} + +func (t *TerminalBase) handlePaddingMsg(c *container.Container) { + padding := c.GetAll() + if len(padding) > 0 { + rngFeeder.SupplyEntropyIfNeeded(padding, len(padding)) + } +} + +func (t *TerminalBase) sendOpMsgs(msg *Msg) *Error { + msg.Unit.WaitForSlot() + + // Add Padding if needed. + if t.opts.Padding > 0 { + paddingNeeded := (int(t.opts.Padding) - msg.Data.Length()) % int(t.opts.Padding) + if paddingNeeded > 0 { + // Add padding message header. + msg.Data.Append([]byte{0}) + paddingNeeded-- + + // Add needed padding data. + if paddingNeeded > 0 { + padding, err := rng.Bytes(paddingNeeded) + if err != nil { + log.Debugf("spn/terminal: %s failed to get random data, using zeros instead", t.FmtID()) + padding = make([]byte, paddingNeeded) + } + msg.Data.Append(padding) + } + } + } + + // Encrypt operative data. + var tErr *Error + msg.Data, tErr = t.encrypt(msg.Data) + if tErr != nil { + return tErr + } + + // Send data. + t.submit(msg, 0) + return nil +} + +// Abandon shuts down the terminal unregistering it from upstream and calling HandleAbandon(). +// Should not be overridden by implementations. +func (t *TerminalBase) Abandon(err *Error) { + if t.Abandoning.SetToIf(false, true) { + module.StartWorker("terminal abandon procedure", func(_ context.Context) error { + t.handleAbandonProcedure(err) + return nil + }) + } +} + +// HandleAbandon gives the terminal the ability to cleanly shut down. +// The returned error is the error to send to the other side. +// Should never be called directly. Call Abandon() instead. +// Meant to be overridden by implementations. +func (t *TerminalBase) HandleAbandon(err *Error) (errorToSend *Error) { + return err +} + +// HandleDestruction gives the terminal the ability to clean up. +// The terminal has already fully shut down at this point. +// Should never be called directly. Call Abandon() instead. +// Meant to be overridden by implementations. +func (t *TerminalBase) HandleDestruction(err *Error) {} + +func (t *TerminalBase) handleAbandonProcedure(err *Error) { + // End all operations. + for _, op := range t.allOps() { + t.StopOperation(op, nil) + } + + // Prepare timeouts for waiting for ops. + timeout := clientTerminalAbandonTimeout + if conf.PublicHub() { + timeout = serverTerminalAbandonTimeout + } + checkTicker := time.NewTicker(50 * time.Millisecond) + defer checkTicker.Stop() + abortWaiting := time.After(timeout) + + // Wait for all operations to end. +waitForOps: + for { + select { + case <-checkTicker.C: + if t.GetActiveOpCount() <= 0 { + break waitForOps + } + case <-abortWaiting: + log.Warningf( + "spn/terminal: terminal %s is continuing shutdown with %d active operations", + t.FmtID(), + t.GetActiveOpCount(), + ) + break waitForOps + } + } + + // Call operation stop handle function for proper shutdown cleaning up. + if t.ext != nil { + err = t.ext.HandleAbandon(err) + } + + // Send error to the connected Operation, if the error is internal. + if !err.IsExternal() { + if err == nil { + err = ErrStopping + } + + msg := NewMsg(err.Pack()) + msg.FlowID = t.ID() + msg.Type = MsgTypeStop + t.submit(msg, 1*time.Second) + } + + // If terminal was ended locally, send all data before abandoning. + // If terminal was ended remotely, don't bother sending remaining data. + if !err.IsExternal() { + // Flushing could mean sending a full buffer of 50000 packets. + t.Flush(5 * time.Minute) + } + + // Stop all other connected workers. + t.cancelCtx() + t.idleTicker.Stop() + + // Call operation destruction handle function for proper shutdown cleaning up. + if t.ext != nil { + t.ext.HandleDestruction(err) + } +} + +func (t *TerminalBase) allOps() []Operation { + t.lock.Lock() + defer t.lock.Unlock() + + ops := make([]Operation, 0, len(t.operations)) + for _, op := range t.operations { + ops = append(ops, op) + } + + return ops +} + +// MakeDirectDeliveryDeliverFunc creates a submit upstream function with the +// given delivery channel. +func MakeDirectDeliveryDeliverFunc( + ctx context.Context, + deliver chan *Msg, +) func(c *Msg) *Error { + return func(c *Msg) *Error { + select { + case deliver <- c: + return nil + case <-ctx.Done(): + return ErrStopping + } + } +} + +// MakeDirectDeliveryRecvFunc makes a delivery receive function with the given +// delivery channel. +func MakeDirectDeliveryRecvFunc( + deliver chan *Msg, +) func() <-chan *Msg { + return func() <-chan *Msg { + return deliver + } +} diff --git a/spn/terminal/terminal_test.go b/spn/terminal/terminal_test.go new file mode 100644 index 00000000..b458f696 --- /dev/null +++ b/spn/terminal/terminal_test.go @@ -0,0 +1,311 @@ +package terminal + +import ( + "fmt" + "os" + "runtime/pprof" + "sync/atomic" + "testing" + "time" + + "github.com/safing/portbase/container" + "github.com/safing/portmaster/spn/cabin" + "github.com/safing/portmaster/spn/hub" +) + +func TestTerminals(t *testing.T) { + t.Parallel() + + identity, erro := cabin.CreateIdentity(module.Ctx, "test") + if erro != nil { + t.Fatalf("failed to create identity: %s", erro) + } + + // Test without and with encryption. + for _, encrypt := range []bool{false, true} { + // Test with different flow controls. + for _, fc := range []struct { + flowControl FlowControlType + flowControlSize uint32 + }{ + { + flowControl: FlowControlNone, + flowControlSize: 5, + }, + { + flowControl: FlowControlDFQ, + flowControlSize: defaultTestQueueSize, + }, + } { + // Run tests with combined options. + testTerminals(t, identity, &TerminalOpts{ + Encrypt: encrypt, + Padding: defaultTestPadding, + FlowControl: fc.flowControl, + FlowControlSize: fc.flowControlSize, + }) + } + } +} + +func testTerminals(t *testing.T, identity *cabin.Identity, terminalOpts *TerminalOpts) { + t.Helper() + + // Prepare encryption. + var dstHub *hub.Hub + if terminalOpts.Encrypt { + dstHub = identity.Hub + } else { + identity = nil + } + + // Create test terminals. + var term1 *TestTerminal + var term2 *TestTerminal + var initData *container.Container + var err *Error + term1, initData, err = NewLocalTestTerminal( + module.Ctx, 127, "c1", dstHub, terminalOpts, createForwardingUpstream( + t, "c1", "c2", func(msg *Msg) *Error { + return term2.Deliver(msg) + }, + ), + ) + if err != nil { + t.Fatalf("failed to create local terminal: %s", err) + } + term2, _, err = NewRemoteTestTerminal( + module.Ctx, 127, "c2", identity, initData, createForwardingUpstream( + t, "c2", "c1", func(msg *Msg) *Error { + return term1.Deliver(msg) + }, + ), + ) + if err != nil { + t.Fatalf("failed to create remote terminal: %s", err) + } + + // Start testing with counters. + countToQueueSize := uint64(terminalOpts.FlowControlSize) + optionsSuffix := fmt.Sprintf( + "encrypt=%v,flowType=%d", + terminalOpts.Encrypt, + terminalOpts.FlowControl, + ) + + testTerminalWithCounters(t, term1, term2, &testWithCounterOpts{ + testName: "onlyup-flushing-waiting:" + optionsSuffix, + flush: true, + serverCountTo: countToQueueSize * 2, + waitBetweenMsgs: sendThresholdMaxWait * 2, + }) + + testTerminalWithCounters(t, term1, term2, &testWithCounterOpts{ + testName: "onlyup-waiting:" + optionsSuffix, + serverCountTo: 10, + waitBetweenMsgs: sendThresholdMaxWait * 2, + }) + + testTerminalWithCounters(t, term1, term2, &testWithCounterOpts{ + testName: "onlyup-flushing:" + optionsSuffix, + flush: true, + serverCountTo: countToQueueSize * 2, + waitBetweenMsgs: time.Millisecond, + }) + + testTerminalWithCounters(t, term1, term2, &testWithCounterOpts{ + testName: "onlyup:" + optionsSuffix, + serverCountTo: countToQueueSize * 2, + waitBetweenMsgs: time.Millisecond, + }) + + testTerminalWithCounters(t, term1, term2, &testWithCounterOpts{ + testName: "onlydown-flushing-waiting:" + optionsSuffix, + flush: true, + clientCountTo: countToQueueSize * 2, + waitBetweenMsgs: sendThresholdMaxWait * 2, + }) + + testTerminalWithCounters(t, term1, term2, &testWithCounterOpts{ + testName: "onlydown-waiting:" + optionsSuffix, + clientCountTo: 10, + waitBetweenMsgs: sendThresholdMaxWait * 2, + }) + + testTerminalWithCounters(t, term1, term2, &testWithCounterOpts{ + testName: "onlydown-flushing:" + optionsSuffix, + flush: true, + clientCountTo: countToQueueSize * 2, + waitBetweenMsgs: time.Millisecond, + }) + + testTerminalWithCounters(t, term1, term2, &testWithCounterOpts{ + testName: "onlydown:" + optionsSuffix, + clientCountTo: countToQueueSize * 2, + waitBetweenMsgs: time.Millisecond, + }) + + testTerminalWithCounters(t, term1, term2, &testWithCounterOpts{ + testName: "twoway-flushing-waiting:" + optionsSuffix, + flush: true, + clientCountTo: countToQueueSize * 2, + serverCountTo: countToQueueSize * 2, + waitBetweenMsgs: sendThresholdMaxWait * 2, + }) + + testTerminalWithCounters(t, term1, term2, &testWithCounterOpts{ + testName: "twoway-waiting:" + optionsSuffix, + flush: true, + clientCountTo: 10, + serverCountTo: 10, + waitBetweenMsgs: sendThresholdMaxWait * 2, + }) + + testTerminalWithCounters(t, term1, term2, &testWithCounterOpts{ + testName: "twoway-flushing:" + optionsSuffix, + flush: true, + clientCountTo: countToQueueSize * 2, + serverCountTo: countToQueueSize * 2, + waitBetweenMsgs: time.Millisecond, + }) + + testTerminalWithCounters(t, term1, term2, &testWithCounterOpts{ + testName: "twoway:" + optionsSuffix, + clientCountTo: countToQueueSize * 2, + serverCountTo: countToQueueSize * 2, + waitBetweenMsgs: time.Millisecond, + }) + + testTerminalWithCounters(t, term1, term2, &testWithCounterOpts{ + testName: "stresstest-down:" + optionsSuffix, + clientCountTo: countToQueueSize * 1000, + }) + + testTerminalWithCounters(t, term1, term2, &testWithCounterOpts{ + testName: "stresstest-up:" + optionsSuffix, + serverCountTo: countToQueueSize * 1000, + }) + + testTerminalWithCounters(t, term1, term2, &testWithCounterOpts{ + testName: "stresstest-duplex:" + optionsSuffix, + clientCountTo: countToQueueSize * 1000, + serverCountTo: countToQueueSize * 1000, + }) + + // Clean up. + term1.Abandon(nil) + term2.Abandon(nil) + + // Give some time for the last log messages and clean up. + time.Sleep(100 * time.Millisecond) +} + +func createForwardingUpstream(t *testing.T, srcName, dstName string, deliverFunc func(*Msg) *Error) Upstream { + t.Helper() + + return UpstreamSendFunc(func(msg *Msg, _ time.Duration) *Error { + // Fast track nil containers. + if msg == nil { + dErr := deliverFunc(msg) + if dErr != nil { + t.Errorf("%s>%s: failed to deliver nil msg to terminal: %s", srcName, dstName, dErr) + return dErr.With("failed to deliver nil msg to terminal") + } + return nil + } + + // Log messages. + if logTestCraneMsgs { + t.Logf("%s>%s: %v\n", srcName, dstName, msg.Data.CompileData()) + } + + // Deliver to other terminal. + dErr := deliverFunc(msg) + if dErr != nil { + t.Errorf("%s>%s: failed to deliver to terminal: %s", srcName, dstName, dErr) + return dErr.With("failed to deliver to terminal") + } + + return nil + }) +} + +type testWithCounterOpts struct { + testName string + flush bool + clientCountTo uint64 + serverCountTo uint64 + waitBetweenMsgs time.Duration +} + +func testTerminalWithCounters(t *testing.T, term1, term2 *TestTerminal, opts *testWithCounterOpts) { + t.Helper() + + // Wait async for test to complete, print stack after timeout. + finished := make(chan struct{}) + maxTestDuration := 60 * time.Second + go func() { + select { + case <-finished: + case <-time.After(maxTestDuration): + fmt.Printf("terminal test %s is taking more than %s, printing stack:\n", opts.testName, maxTestDuration) + _ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) + os.Exit(1) + } + }() + + t.Logf("starting terminal counter test %s", opts.testName) + defer t.Logf("stopping terminal counter test %s", opts.testName) + + // Start counters. + counter, tErr := NewCounterOp(term1, CounterOpts{ + ClientCountTo: opts.clientCountTo, + ServerCountTo: opts.serverCountTo, + Flush: opts.flush, + Wait: opts.waitBetweenMsgs, + }) + if tErr != nil { + t.Fatalf("terminal test %s failed to start counter: %s", opts.testName, tErr) + } + + // Wait until counters are done. + counter.Wait() + close(finished) + + // Check for error. + if counter.Error != nil { + t.Fatalf("terminal test %s failed to count: %s", opts.testName, counter.Error) + } + + // Log stats. + printCTStats(t, opts.testName, "term1", term1) + printCTStats(t, opts.testName, "term2", term2) + + // Check if stats match, if DFQ is used on both sides. + dfq1, ok1 := term1.flowControl.(*DuplexFlowQueue) + dfq2, ok2 := term2.flowControl.(*DuplexFlowQueue) + if ok1 && ok2 && + (atomic.LoadInt32(dfq1.sendSpace) != atomic.LoadInt32(dfq2.reportedSpace) || + atomic.LoadInt32(dfq2.sendSpace) != atomic.LoadInt32(dfq1.reportedSpace)) { + t.Fatalf("terminal test %s has non-matching space counters", opts.testName) + } +} + +func printCTStats(t *testing.T, testName, name string, term *TestTerminal) { + t.Helper() + + dfq, ok := term.flowControl.(*DuplexFlowQueue) + if !ok { + return + } + + t.Logf( + "%s: %s: sq=%d rq=%d sends=%d reps=%d", + testName, + name, + len(dfq.sendQueue), + len(dfq.recvQueue), + atomic.LoadInt32(dfq.sendSpace), + atomic.LoadInt32(dfq.reportedSpace), + ) +} diff --git a/spn/terminal/testing.go b/spn/terminal/testing.go new file mode 100644 index 00000000..22b12608 --- /dev/null +++ b/spn/terminal/testing.go @@ -0,0 +1,243 @@ +package terminal + +import ( + "context" + "time" + + "github.com/safing/portbase/container" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/spn/cabin" + "github.com/safing/portmaster/spn/hub" +) + +const ( + defaultTestQueueSize = 16 + defaultTestPadding = 8 + logTestCraneMsgs = false +) + +// TestTerminal is a terminal for running tests. +type TestTerminal struct { + *TerminalBase +} + +// NewLocalTestTerminal returns a new local test terminal. +func NewLocalTestTerminal( + ctx context.Context, + id uint32, + parentID string, + remoteHub *hub.Hub, + initMsg *TerminalOpts, + upstream Upstream, +) (*TestTerminal, *container.Container, *Error) { + // Create Terminal Base. + t, initData, err := NewLocalBaseTerminal(ctx, id, parentID, remoteHub, initMsg, upstream) + if err != nil { + return nil, nil, err + } + t.StartWorkers(module, "test terminal") + + return &TestTerminal{t}, initData, nil +} + +// NewRemoteTestTerminal returns a new remote test terminal. +func NewRemoteTestTerminal( + ctx context.Context, + id uint32, + parentID string, + identity *cabin.Identity, + initData *container.Container, + upstream Upstream, +) (*TestTerminal, *TerminalOpts, *Error) { + // Create Terminal Base. + t, initMsg, err := NewRemoteBaseTerminal(ctx, id, parentID, identity, initData, upstream) + if err != nil { + return nil, nil, err + } + t.StartWorkers(module, "test terminal") + + return &TestTerminal{t}, initMsg, nil +} + +type delayedMsg struct { + msg *Msg + timeout time.Duration + delayUntil time.Time +} + +func createDelayingTestForwardingFunc( + srcName, + dstName string, + delay time.Duration, + delayQueueSize int, + deliverFunc func(msg *Msg, timeout time.Duration) *Error, +) func(msg *Msg, timeout time.Duration) *Error { + // Return simple forward func if no delay is given. + if delay == 0 { + return func(msg *Msg, timeout time.Duration) *Error { + // Deliver to other terminal. + dErr := deliverFunc(msg, timeout) + if dErr != nil { + log.Errorf("spn/testing: %s>%s: failed to deliver to terminal: %s", srcName, dstName, dErr) + return dErr + } + return nil + } + } + + // If there is delay, create a delaying channel and handler. + delayedMsgs := make(chan *delayedMsg, delayQueueSize) + go func() { + for { + // Read from chan + msg := <-delayedMsgs + if msg == nil { + return + } + + // Check if we need to wait. + waitFor := time.Until(msg.delayUntil) + if waitFor > 0 { + time.Sleep(waitFor) + } + + // Deliver to other terminal. + dErr := deliverFunc(msg.msg, msg.timeout) + if dErr != nil { + log.Errorf("spn/testing: %s>%s: failed to deliver to terminal: %s", srcName, dstName, dErr) + } + } + }() + + return func(msg *Msg, timeout time.Duration) *Error { + // Add msg to delaying msg channel. + delayedMsgs <- &delayedMsg{ + msg: msg, + timeout: timeout, + delayUntil: time.Now().Add(delay), + } + return nil + } +} + +// HandleAbandon gives the terminal the ability to cleanly shut down. +// The returned error is the error to send to the other side. +// Should never be called directly. Call Abandon() instead. +func (t *TestTerminal) HandleAbandon(err *Error) (errorToSend *Error) { + switch err { + case nil: + // nil means that the Terminal is being shutdown by the owner. + log.Tracef("spn/terminal: %s is closing", fmtTerminalID(t.parentID, t.id)) + default: + // All other errors are faults. + log.Warningf("spn/terminal: %s: %s", fmtTerminalID(t.parentID, t.id), err) + } + + return +} + +// NewSimpleTestTerminalPair provides a simple conntected terminal pair for tests. +func NewSimpleTestTerminalPair(delay time.Duration, delayQueueSize int, opts *TerminalOpts) (a, b *TestTerminal, err error) { + if opts == nil { + opts = &TerminalOpts{ + Padding: defaultTestPadding, + FlowControl: FlowControlDFQ, + FlowControlSize: defaultTestQueueSize, + } + } + + var initData *container.Container + var tErr *Error + a, initData, tErr = NewLocalTestTerminal( + module.Ctx, 127, "a", nil, opts, UpstreamSendFunc(createDelayingTestForwardingFunc( + "a", "b", delay, delayQueueSize, func(msg *Msg, timeout time.Duration) *Error { + return b.Deliver(msg) + }, + )), + ) + if tErr != nil { + return nil, nil, tErr.Wrap("failed to create local test terminal") + } + b, _, tErr = NewRemoteTestTerminal( + module.Ctx, 127, "b", nil, initData, UpstreamSendFunc(createDelayingTestForwardingFunc( + "b", "a", delay, delayQueueSize, func(msg *Msg, timeout time.Duration) *Error { + return a.Deliver(msg) + }, + )), + ) + if tErr != nil { + return nil, nil, tErr.Wrap("failed to create remote test terminal") + } + + return a, b, nil +} + +// BareTerminal is a bare terminal that just returns errors for testing. +type BareTerminal struct{} + +var ( + _ Terminal = &BareTerminal{} + + errNotImplementedByBareTerminal = ErrInternalError.With("not implemented by bare terminal") +) + +// ID returns the terminal ID. +func (t *BareTerminal) ID() uint32 { + return 0 +} + +// Ctx returns the terminal context. +func (t *BareTerminal) Ctx() context.Context { + return context.Background() +} + +// Deliver delivers a message to the terminal. +// Should not be overridden by implementations. +func (t *BareTerminal) Deliver(msg *Msg) *Error { + return errNotImplementedByBareTerminal +} + +// Send is used by others to send a message through the terminal. +// Should not be overridden by implementations. +func (t *BareTerminal) Send(msg *Msg, timeout time.Duration) *Error { + return errNotImplementedByBareTerminal +} + +// Flush sends all messages waiting in the terminal. +// Should not be overridden by implementations. +func (t *BareTerminal) Flush(timeout time.Duration) {} + +// StartOperation starts the given operation by assigning it an ID and sending the given operation initialization data. +// Should not be overridden by implementations. +func (t *BareTerminal) StartOperation(op Operation, initData *container.Container, timeout time.Duration) *Error { + return errNotImplementedByBareTerminal +} + +// StopOperation stops the given operation. +// Should not be overridden by implementations. +func (t *BareTerminal) StopOperation(op Operation, err *Error) {} + +// Abandon shuts down the terminal unregistering it from upstream and calling HandleAbandon(). +// Should not be overridden by implementations. +func (t *BareTerminal) Abandon(err *Error) {} + +// HandleAbandon gives the terminal the ability to cleanly shut down. +// The terminal is still fully functional at this point. +// The returned error is the error to send to the other side. +// Should never be called directly. Call Abandon() instead. +// Meant to be overridden by implementations. +func (t *BareTerminal) HandleAbandon(err *Error) (errorToSend *Error) { + return err +} + +// HandleDestruction gives the terminal the ability to clean up. +// The terminal has already fully shut down at this point. +// Should never be called directly. Call Abandon() instead. +// Meant to be overridden by implementations. +func (t *BareTerminal) HandleDestruction(err *Error) {} + +// FmtID formats the terminal ID (including parent IDs). +// May be overridden by implementations. +func (t *BareTerminal) FmtID() string { + return "bare" +} diff --git a/spn/terminal/upstream.go b/spn/terminal/upstream.go new file mode 100644 index 00000000..9dd27d43 --- /dev/null +++ b/spn/terminal/upstream.go @@ -0,0 +1,16 @@ +package terminal + +import "time" + +// Upstream defines the interface for upstream (parent) components. +type Upstream interface { + Send(msg *Msg, timeout time.Duration) *Error +} + +// UpstreamSendFunc is a helper to be able to satisfy the Upstream interface. +type UpstreamSendFunc func(msg *Msg, timeout time.Duration) *Error + +// Send is used to send a message through this upstream. +func (fn UpstreamSendFunc) Send(msg *Msg, timeout time.Duration) *Error { + return fn(msg, timeout) +} diff --git a/spn/test b/spn/test new file mode 100755 index 00000000..2a443bb4 --- /dev/null +++ b/spn/test @@ -0,0 +1,168 @@ +#!/bin/bash + +warnings=0 +errors=0 +scripted=0 +goUp="\\e[1A" +fullTestFlags="-short" +install=0 +testonly=0 + +function help { + echo "usage: $0 [command] [options]" + echo "" + echo "commands:" + echo " run baseline tests" + echo " full run full tests (ie. not short)" + echo " install install deps for running tests" + echo "" + echo "options:" + echo " --scripted don't jump console lines (still use colors)" + echo " --test-only run tests only, no linters" + echo " [package] run only on this package" +} + +function run { + if [[ $scripted -eq 0 ]]; then + echo "[......] $*" + fi + + # create tmpfile + tmpfile=$(mktemp) + # execute + $* >$tmpfile 2>&1 + rc=$? + output=$(cat $tmpfile) + + # check return code + if [[ $rc -eq 0 ]]; then + if [[ $output == *"[no test files]"* ]]; then + echo -e "${goUp}[\e[01;33mNOTEST\e[00m] $*" + warnings=$((warnings+1)) + else + echo -ne "${goUp}[\e[01;32m OK \e[00m] " + if [[ $2 == "test" ]]; then + echo -n $* + echo -n ": " + echo $output | cut -f "3-" -d " " + else + echo $* + fi + fi + else + if [[ $output == *"build constraints exclude all Go files"* ]]; then + echo -e "${goUp}[ !=OS ] $*" + else + echo -e "${goUp}[\e[01;31m FAIL \e[00m] $*" + cat $tmpfile + errors=$((errors+1)) + fi + fi + + rm -f $tmpfile +} + +# get and switch to script dir +baseDir="$( cd "$(dirname "$0")" && pwd )" +cd "$baseDir" + +# args +while true; do + case "$1" in + "-h"|"help"|"--help") + help + exit 0 + ;; + "--scripted") + scripted=1 + goUp="" + shift 1 + ;; + "--test-only") + testonly=1 + shift 1 + ;; + "install") + install=1 + shift 1 + ;; + "full") + fullTestFlags="" + shift 1 + ;; + *) + break + ;; + esac +done + +# check if $GOPATH/bin is in $PATH +if [[ $PATH != *"$GOPATH/bin"* ]]; then + export PATH=$GOPATH/bin:$PATH +fi + +# install +if [[ $install -eq 1 ]]; then + echo "installing dependencies..." + # TODO: update golangci-lint version regularly + echo "$ curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.44.0" + curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.44.0 + exit 0 +fi + +# check dependencies +if [[ $(which go) == "" ]]; then + echo "go command not found" + exit 1 +fi +if [[ $testonly -eq 0 ]]; then + if [[ $(which gofmt) == "" ]]; then + echo "gofmt command not found" + exit 1 + fi + if [[ $(which golangci-lint) == "" ]]; then + echo "golangci-lint command not found" + echo "install with: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin vX.Y.Z" + echo "don't forget to specify the version you want" + echo "or run: ./test install" + echo "" + echo "alternatively, install the current dev version with: go get -u github.com/golangci/golangci-lint/cmd/golangci-lint" + exit 1 + fi +fi + +# target selection +if [[ "$1" == "" ]]; then + # get all packages + packages=$(go list -e ./...) +else + # single package testing + packages=$(go list -e)/$1 + echo "note: only running tests for package $packages" +fi + +# platform info +platformInfo=$(go env GOOS GOARCH) +echo "running tests for ${platformInfo//$'\n'/ }:" + +# run vet/test on packages +for package in $packages; do + packagename=${package#github.com/safing/spn} #TODO: could be queried with `go list .` + packagename=${packagename#/} + echo "" + echo $package + if [[ $testonly -eq 0 ]]; then + run go vet $package + run golangci-lint run $packagename + fi + run go test -cover $fullTestFlags $package +done + +echo "" +if [[ $errors -gt 0 ]]; then + echo "failed with $errors errors and $warnings warnings" + exit 1 +else + echo "succeeded with $warnings warnings" + exit 0 +fi diff --git a/spn/tools/Dockerfile b/spn/tools/Dockerfile new file mode 100644 index 00000000..dbe39af1 --- /dev/null +++ b/spn/tools/Dockerfile @@ -0,0 +1,23 @@ +FROM alpine as builder + +# Ensure ca-certficates are up to date +# RUN update-ca-certificates + +# Download and verify portmaster-start binary. +RUN mkdir /init +RUN wget https://updates.safing.io/linux_amd64/start/portmaster-start_v0-9-6 -O /init/portmaster-start +COPY start-checksum.txt /init/start-checksum +RUN cd /init && sha256sum -c /init/start-checksum +RUN chmod 555 /init/portmaster-start + +# Use minimal image as base. +FROM alpine + +# Copy the static executable. +COPY --from=builder /init/portmaster-start /init/portmaster-start + +# Copy the init script +COPY container-init.sh /init.sh + +# Run the hub. +ENTRYPOINT ["/init.sh"] diff --git a/spn/tools/container-init.sh b/spn/tools/container-init.sh new file mode 100755 index 00000000..e5120872 --- /dev/null +++ b/spn/tools/container-init.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +DATA="/data" +START="/data/portmaster-start" +INIT_START="/init/portmaster-start" + +# Set safe shell options. +set -euf -o pipefail + +# Check if data dir is mounted. +if [ ! -d $DATA ]; then + echo "Nothing mounted at $DATA, aborting." + exit 1 +fi + +# Copy init start to correct location, if not available. +if [ ! -f $START ]; then + cp $INIT_START $START +fi + +# Download updates. +echo "running: $START update --data /data --intel-only" +$START update --data /data --intel-only + +# Remove PID file, which could have been left after a crash. +rm -f $DATA/hub-lock.pid + +# Always start the SPN Hub with the updated main start binary. +echo "running: $START hub --data /data -- $@" +$START hub --data /data -- $@ diff --git a/spn/tools/install.sh b/spn/tools/install.sh new file mode 100755 index 00000000..e7cf8fd7 --- /dev/null +++ b/spn/tools/install.sh @@ -0,0 +1,326 @@ +#!/bin/sh +# +# This script should be run via curl as root: +# sudo sh -c "$(curl -fsSL https://raw.githubusercontent.com/safing/portmaster/master/spn/tools/install-spn.sh)" +# or wget +# sudo sh -c "$(wget -qO- https://raw.githubusercontent.com/safing/portmaster/master/spn/tools/install-spn.sh)" +# +# As an alternative, you can first download the install script and run it afterwards: +# wget https://raw.githubusercontent.com/safing/portmaster/master/spn/tools/install-spn.sh +# sudo sh ./install.sh +# +# +set -e + +ARCH= +INSTALLDIR= +PMSTART= +ENABLENOW= +INSTALLSYSTEMD= +SYSTEMDINSTALLPATH= + +apply_defaults() { + ARCH=${ARCH:-amd64} + INSTALLDIR=${INSTALLDIR:-/opt/safing/spn} + PMSTART=${PMSTART:-https://updates.safing.io/latest/linux_${ARCH}/start/portmaster-start} + SYSTEMDINSTALLPATH=${SYSTEMDINSTALLPATH:-/etc/systemd/system/spn.service} + + if command_exists systemctl; then + INSTALLSYSTEMD=${INSTALLSYSTEMD:-yes} + ENABLENOW=${ENABLENOW:-yes} + else + INSTALLSYSTEMD=${INSTALLSYSTEMD:-no} + ENABLENOW=${ENABLENOW:-no} + fi + + # The hostname may be freshly set, ensure the ENV variable is correct. + export HOSTNAME=$(hostname) +} + +command_exists() { + command -v "$@" >/dev/null 2>&1 +} + +setup_tty() { + if [ -t 0 ]; then + interactive=yes + fi + + if [ -t 1 ]; then + RED=$(printf '\033[31m') + GREEN=$(printf '\033[32m') + YELLOW=$(printf '\033[33m') + BLUE=$(printf '\033[34m') + BOLD=$(printf '\033[1m') + RESET=$(printf '\033[m') + else + RED="" + GREEN="" + YELLOW="" + BLUE="" + BOLD="" + RESET="" + fi +} + +log() { + echo ${GREEN}${BOLD}"-> "${RESET}"$@" >&2 +} + +error() { + echo ${RED}"Error: $@"${RESET} >&2 +} + +warn() { + echo ${YELLOW}"warn: $@"${RESET} >&2 +} + +run_systemctl() { + systemctl $@ >/dev/null 2>&1 +} + +download_file() { + local src=$1 + local dest=$2 + + if command_exists curl; then + curl --silent --fail --show-error --location --output $dest $src + elif command_exists wget; then + wget --quiet -O $dest $src + else + error "No suitable download command found, either curl or wget must be installed" + exit 1 + fi +} + +ensure_install_dir() { + log "Creating ${INSTALLDIR}" + mkdir -p ${INSTALLDIR} +} + +download_pmstart() { + log "Downloading portmaster-start ..." + local dest="${INSTALLDIR}/portmaster-start" + if [ -f "${dest}" ]; then + warn "Overwriting existing portmaster-start at ${dest}" + fi + + download_file ${PMSTART} ${dest} + + log "Changing permissions" + chmod a+x ${dest} +} + +download_updates() { + log "Downloading updates ..." + ${INSTALLDIR}/portmaster-start --data=${INSTALLDIR} update +} + +setup_systemd() { + log "Installing systemd service unit ..." + if [ ! "${INSTALLSYSTEMD}" = "yes" ]; then + warn "Skipping setup of systemd service unit" + echo "To launch the hub, execute the following as root:" + echo "" + echo "${INSTALLDIR}/portmaster-start --data ${INSTALLDIR} hub" + echo "" + return + fi + + if [ -f "${SYSTEMDINSTALLPATH}" ]; then + warn "Overwriting existing unit path" + fi + + cat >${SYSTEMDINSTALLPATH} < " HOSTNAME + fi + if [ "${METRICS_COMMENT}" = "" ]; then + log "Please enter metrics comment:" + read -p "> " METRICS_COMMENT + fi +} + +write_config_file() { + cat >${1} < /etc/sysctl.d/9999-spn-network-optimizing.conf +# cat /etc/sysctl.d/9999-spn-network-optimizing.conf +# sysctl -p /etc/sysctl.d/9999-spn-network-optimizing.conf + +# Provide adequate buffer memory. +# net.ipv4.tcp_mem is in 4096-byte pages. +net.core.rmem_max = 1073741824 +net.core.wmem_max = 1073741824 +net.core.rmem_default = 16777216 +net.core.wmem_default = 16777216 +net.ipv4.tcp_rmem = 4096 16777216 1073741824 +net.ipv4.tcp_wmem = 4096 16777216 1073741824 +net.ipv4.tcp_mem = 4194304 8388608 16777216 +net.ipv4.udp_rmem_min = 16777216 +net.ipv4.udp_wmem_min = 16777216 + +# Enable TCP window scaling. +net.ipv4.tcp_window_scaling = 1 + +# Increase the length of the processor input queue +net.core.netdev_max_backlog = 100000 +net.core.netdev_budget = 1000 +net.core.netdev_budget_usecs = 10000 + +# Set better congestion control. +net.ipv4.tcp_congestion_control = htcp + +# Turn off fancy stuff for more stability. +net.ipv4.tcp_sack = 0 +net.ipv4.tcp_dsack = 0 +net.ipv4.tcp_fack = 0 +net.ipv4.tcp_timestamps = 0 + +# Max reorders before slow start. +net.ipv4.tcp_reordering = 3 + +# Prefer low latency to higher throughput. +# Disables IPv4 TCP prequeue processing. +net.ipv4.tcp_low_latency = 1 + +# Don't start slow. +net.ipv4.tcp_slow_start_after_idle = 0 diff --git a/spn/unit/doc.go b/spn/unit/doc.go new file mode 100644 index 00000000..9826a6ce --- /dev/null +++ b/spn/unit/doc.go @@ -0,0 +1,13 @@ +// Package unit provides a "work unit" scheduling system for handling data sets that traverse multiple workers / goroutines. +// The aim is to bind priority to a data set instead of a goroutine and split resources fairly among requests. +// +// Every "work" Unit is assigned an ever increasing ID and can be marked as "paused" or "high priority". +// The Scheduler always gives a clearance up to a certain ID. All units below this ID may be processed. +// High priority Units may always be processed. +// +// The Scheduler works with short slots and measures how many Units were finished in a slot. +// The "slot pace" holds an indication of the current Unit finishing speed per slot. It is only changed slowly (but boosts if too far away) in order to keep stabilize the system. +// The Scheduler then calculates the next unit ID limit to give clearance to for the next slot: +// +// "finished units" + "slot pace" + "paused units" - "fraction of high priority units" +package unit diff --git a/spn/unit/scheduler.go b/spn/unit/scheduler.go new file mode 100644 index 00000000..0b5d6e11 --- /dev/null +++ b/spn/unit/scheduler.go @@ -0,0 +1,358 @@ +package unit + +import ( + "context" + "errors" + "math" + "sync" + "sync/atomic" + "time" + + "github.com/tevino/abool" +) + +const ( + defaultSlotDuration = 10 * time.Millisecond // 100 slots per second + defaultMinSlotPace = 100 // 10 000 pps + + defaultWorkSlotPercentage = 0.7 // 70% + defaultSlotChangeRatePerStreak = 0.02 // 2% + + defaultStatCycleDuration = 1 * time.Minute +) + +// Scheduler creates and schedules units. +// Must be created using NewScheduler(). +type Scheduler struct { //nolint:maligned + // Configuration. + config SchedulerConfig + + // Units IDs Limit / Thresholds. + + // currentUnitID holds the last assigned Unit ID. + currentUnitID atomic.Int64 + // clearanceUpTo holds the current threshold up to which Unit ID Units may be processed. + clearanceUpTo atomic.Int64 + // slotPace holds the current pace. This is the base value for clearance + // calculation, not the value of the current cleared Units itself. + slotPace atomic.Int64 + // finished holds the amount of units that were finished within the current slot. + finished atomic.Int64 + + // Slot management. + slotSignalA chan struct{} + slotSignalB chan struct{} + slotSignalSwitch bool + slotSignalsLock sync.RWMutex + + stopping abool.AtomicBool + unitDebugger *UnitDebugger + + // Stats. + stats struct { + // Working Values. + progress struct { + maxPace atomic.Int64 + maxLeveledPace atomic.Int64 + avgPaceSum atomic.Int64 + avgPaceCnt atomic.Int64 + avgUnitLifeSum atomic.Int64 + avgUnitLifeCnt atomic.Int64 + avgWorkSlotSum atomic.Int64 + avgWorkSlotCnt atomic.Int64 + avgCatchUpSlotSum atomic.Int64 + avgCatchUpSlotCnt atomic.Int64 + } + + // Calculated Values. + current struct { + maxPace atomic.Int64 + maxLeveledPace atomic.Int64 + avgPace atomic.Int64 + avgUnitLife atomic.Int64 + avgWorkSlot atomic.Int64 + avgCatchUpSlot atomic.Int64 + } + } +} + +// SchedulerConfig holds scheduler configuration. +type SchedulerConfig struct { + // SlotDuration defines the duration of one slot. + SlotDuration time.Duration + + // MinSlotPace defines the minimum slot pace. + // The slot pace will never fall below this value. + MinSlotPace int64 + + // WorkSlotPercentage defines the how much of a slot should be scheduled with work. + // The remainder is for catching up and breathing room for other tasks. + // Must be between 55% (0.55) and 95% (0.95). + // The default value is 0.7 (70%). + WorkSlotPercentage float64 + + // SlotChangeRatePerStreak defines how many percent (0-1) the slot pace + // should change per streak. + // Is enforced to be able to change the minimum slot pace by at least 1. + // The default value is 0.02 (2%). + SlotChangeRatePerStreak float64 + + // StatCycleDuration defines how often stats are calculated. + // The default value is 1 minute. + StatCycleDuration time.Duration +} + +// NewScheduler returns a new scheduler. +func NewScheduler(config *SchedulerConfig) *Scheduler { + // Fallback to empty config if none is given. + if config == nil { + config = &SchedulerConfig{} + } + + // Create new scheduler. + s := &Scheduler{ + config: *config, + slotSignalA: make(chan struct{}), + slotSignalB: make(chan struct{}), + } + + // Fill in defaults. + if s.config.SlotDuration == 0 { + s.config.SlotDuration = defaultSlotDuration + } + if s.config.MinSlotPace == 0 { + s.config.MinSlotPace = defaultMinSlotPace + } + if s.config.WorkSlotPercentage == 0 { + s.config.WorkSlotPercentage = defaultWorkSlotPercentage + } + if s.config.SlotChangeRatePerStreak == 0 { + s.config.SlotChangeRatePerStreak = defaultSlotChangeRatePerStreak + } + if s.config.StatCycleDuration == 0 { + s.config.StatCycleDuration = defaultStatCycleDuration + } + + // Check boundaries of WorkSlotPercentage. + switch { + case s.config.WorkSlotPercentage < 0.55: + s.config.WorkSlotPercentage = 0.55 + case s.config.WorkSlotPercentage > 0.95: + s.config.WorkSlotPercentage = 0.95 + } + + // The slot change rate must be able to change the slot pace by at least 1. + if s.config.SlotChangeRatePerStreak < (1 / float64(s.config.MinSlotPace)) { + s.config.SlotChangeRatePerStreak = (1 / float64(s.config.MinSlotPace)) + + // Debug logging: + // fmt.Printf("--- increased SlotChangeRatePerStreak to %f\n", s.config.SlotChangeRatePerStreak) + } + + // Initialize scheduler fields. + s.clearanceUpTo.Store(s.config.MinSlotPace) + s.slotPace.Store(s.config.MinSlotPace) + + return s +} + +func (s *Scheduler) nextSlotSignal() chan struct{} { + s.slotSignalsLock.RLock() + defer s.slotSignalsLock.RUnlock() + + if s.slotSignalSwitch { + return s.slotSignalA + } + return s.slotSignalB +} + +func (s *Scheduler) announceNextSlot() { + s.slotSignalsLock.Lock() + defer s.slotSignalsLock.Unlock() + + // Close new slot signal and refresh previous one. + if s.slotSignalSwitch { + close(s.slotSignalA) + s.slotSignalB = make(chan struct{}) + } else { + close(s.slotSignalB) + s.slotSignalA = make(chan struct{}) + } + + // Switch to next slot. + s.slotSignalSwitch = !s.slotSignalSwitch +} + +// SlotScheduler manages the slot and schedules units. +// Must only be started once. +func (s *Scheduler) SlotScheduler(ctx context.Context) error { + // Start slot ticker. + ticker := time.NewTicker(s.config.SlotDuration / 2) + defer ticker.Stop() + + // Give clearance to all when stopping. + defer s.clearanceUpTo.Store(math.MaxInt64 - math.MaxInt32) + + var ( + halfSlotID uint64 + halfSlotStartedAt = time.Now() + halfSlotEndedAt time.Time + halfSlotDuration = float64(s.config.SlotDuration / 2) + + increaseStreak float64 + decreaseStreak float64 + oneStreaks int + + cycleStatsAt = uint64(s.config.StatCycleDuration / (s.config.SlotDuration / 2)) + ) + + for range ticker.C { + halfSlotEndedAt = time.Now() + + switch { + case halfSlotID%2 == 0: + + // First Half-Slot: Work Slot + + // Calculate time taken in previous slot. + catchUpSlotDuration := halfSlotEndedAt.Sub(halfSlotStartedAt).Nanoseconds() + + // Add current slot duration to avg calculation. + s.stats.progress.avgCatchUpSlotCnt.Add(1) + if s.stats.progress.avgCatchUpSlotSum.Add(catchUpSlotDuration) < 0 { + // Reset if we wrap. + s.stats.progress.avgCatchUpSlotCnt.Store(1) + s.stats.progress.avgCatchUpSlotSum.Store(catchUpSlotDuration) + } + + // Reset slot counters. + s.finished.Store(0) + + // Raise clearance according + s.clearanceUpTo.Store( + s.currentUnitID.Load() + + int64( + float64(s.slotPace.Load())*s.config.WorkSlotPercentage, + ), + ) + + // Announce start of new slot. + s.announceNextSlot() + + default: + + // Second Half-Slot: Catch-Up Slot + + // Calculate time taken in previous slot. + workSlotDuration := halfSlotEndedAt.Sub(halfSlotStartedAt).Nanoseconds() + + // Add current slot duration to avg calculation. + s.stats.progress.avgWorkSlotCnt.Add(1) + if s.stats.progress.avgWorkSlotSum.Add(workSlotDuration) < 0 { + // Reset if we wrap. + s.stats.progress.avgWorkSlotCnt.Store(1) + s.stats.progress.avgWorkSlotSum.Store(workSlotDuration) + } + + // Calculate slot duration skew correction, as slots will not run in the + // exact specified duration. + slotDurationSkewCorrection := halfSlotDuration / float64(workSlotDuration) + + // Calculate slot pace with performance of first half-slot. + // Get current slot pace as float64. + currentSlotPace := float64(s.slotPace.Load()) + // Calculate current raw slot pace. + newRawSlotPace := float64(s.finished.Load()*2) * slotDurationSkewCorrection + + // Move slot pace in the trending direction. + if newRawSlotPace >= currentSlotPace { + // Adjust based on streak. + increaseStreak++ + decreaseStreak = 0 + s.slotPace.Add(int64( + currentSlotPace * s.config.SlotChangeRatePerStreak * increaseStreak, + )) + + // Count one-streaks. + if increaseStreak == 1 { + oneStreaks++ + } else { + oneStreaks = 0 + } + + // Debug logging: + // fmt.Printf("+++ slot pace: %.0f (current raw pace: %.0f, increaseStreak: %.0f, clearanceUpTo: %d)\n", currentSlotPace, newRawSlotPace, increaseStreak, s.clearanceUpTo.Load()) + } else { + // Adjust based on streak. + decreaseStreak++ + increaseStreak = 0 + s.slotPace.Add(int64( + -currentSlotPace * s.config.SlotChangeRatePerStreak * decreaseStreak, + )) + + // Enforce minimum. + if s.slotPace.Load() < s.config.MinSlotPace { + s.slotPace.Store(s.config.MinSlotPace) + decreaseStreak = 0 + } + + // Count one-streaks. + if decreaseStreak == 1 { + oneStreaks++ + } else { + oneStreaks = 0 + } + + // Debug logging: + // fmt.Printf("--- slot pace: %.0f (current raw pace: %.0f, decreaseStreak: %.0f, clearanceUpTo: %d)\n", currentSlotPace, newRawSlotPace, decreaseStreak, s.clearanceUpTo.Load()) + } + + // Record Stats + + // Add current pace to avg calculation. + s.stats.progress.avgPaceCnt.Add(1) + if s.stats.progress.avgPaceSum.Add(s.slotPace.Load()) < 0 { + // Reset if we wrap. + s.stats.progress.avgPaceCnt.Store(1) + s.stats.progress.avgPaceSum.Store(s.slotPace.Load()) + } + + // Check if current pace is new max. + if s.slotPace.Load() > s.stats.progress.maxPace.Load() { + s.stats.progress.maxPace.Store(s.slotPace.Load()) + } + + // Check if current pace is new leveled max + if oneStreaks >= 3 && s.slotPace.Load() > s.stats.progress.maxLeveledPace.Load() { + s.stats.progress.maxLeveledPace.Store(s.slotPace.Load()) + } + } + // Switch to other slot-half. + halfSlotID++ + halfSlotStartedAt = halfSlotEndedAt + + // Cycle stats after defined time period. + if halfSlotID%cycleStatsAt == 0 { + s.cycleStats() + } + + // Check if we are stopping. + select { + case <-ctx.Done(): + return nil + default: + } + if s.stopping.IsSet() { + return nil + } + } + + // We should never get here. + // If we do, trigger a worker restart via the service worker. + return errors.New("unexpected end of scheduler") +} + +// Stop stops the scheduler and gives clearance to all units. +func (s *Scheduler) Stop() { + s.stopping.Set() +} diff --git a/spn/unit/scheduler_stats.go b/spn/unit/scheduler_stats.go new file mode 100644 index 00000000..6fd1d272 --- /dev/null +++ b/spn/unit/scheduler_stats.go @@ -0,0 +1,87 @@ +package unit + +// Stats are somewhat racy, as one value of sum or count might already be +// updated with the latest slot data, while the other has been not. +// This is not so much of a problem, as slots are really short and the impact +// is very low. + +// cycleStats calculates the new values and cycles the current values. +func (s *Scheduler) cycleStats() { + // Get and reset max pace. + s.stats.current.maxPace.Store(s.stats.progress.maxPace.Load()) + s.stats.progress.maxPace.Store(0) + + // Get and reset max leveled pace. + s.stats.current.maxLeveledPace.Store(s.stats.progress.maxLeveledPace.Load()) + s.stats.progress.maxLeveledPace.Store(0) + + // Get and reset avg slot pace. + avgPaceCnt := s.stats.progress.avgPaceCnt.Load() + if avgPaceCnt > 0 { + s.stats.current.avgPace.Store(s.stats.progress.avgPaceSum.Load() / avgPaceCnt) + } else { + s.stats.current.avgPace.Store(0) + } + s.stats.progress.avgPaceCnt.Store(0) + s.stats.progress.avgPaceSum.Store(0) + + // Get and reset avg unit life. + avgUnitLifeCnt := s.stats.progress.avgUnitLifeCnt.Load() + if avgUnitLifeCnt > 0 { + s.stats.current.avgUnitLife.Store(s.stats.progress.avgUnitLifeSum.Load() / avgUnitLifeCnt) + } else { + s.stats.current.avgUnitLife.Store(0) + } + s.stats.progress.avgUnitLifeCnt.Store(0) + s.stats.progress.avgUnitLifeSum.Store(0) + + // Get and reset avg work slot duration. + avgWorkSlotCnt := s.stats.progress.avgWorkSlotCnt.Load() + if avgWorkSlotCnt > 0 { + s.stats.current.avgWorkSlot.Store(s.stats.progress.avgWorkSlotSum.Load() / avgWorkSlotCnt) + } else { + s.stats.current.avgWorkSlot.Store(0) + } + s.stats.progress.avgWorkSlotCnt.Store(0) + s.stats.progress.avgWorkSlotSum.Store(0) + + // Get and reset avg catch up slot duration. + avgCatchUpSlotCnt := s.stats.progress.avgCatchUpSlotCnt.Load() + if avgCatchUpSlotCnt > 0 { + s.stats.current.avgCatchUpSlot.Store(s.stats.progress.avgCatchUpSlotSum.Load() / avgCatchUpSlotCnt) + } else { + s.stats.current.avgCatchUpSlot.Store(0) + } + s.stats.progress.avgCatchUpSlotCnt.Store(0) + s.stats.progress.avgCatchUpSlotSum.Store(0) +} + +// GetMaxSlotPace returns the current maximum slot pace. +func (s *Scheduler) GetMaxSlotPace() int64 { + return s.stats.current.maxPace.Load() +} + +// GetMaxLeveledSlotPace returns the current maximum leveled slot pace. +func (s *Scheduler) GetMaxLeveledSlotPace() int64 { + return s.stats.current.maxLeveledPace.Load() +} + +// GetAvgSlotPace returns the current average slot pace. +func (s *Scheduler) GetAvgSlotPace() int64 { + return s.stats.current.avgPace.Load() +} + +// GetAvgUnitLife returns the current average unit lifetime until it is finished. +func (s *Scheduler) GetAvgUnitLife() int64 { + return s.stats.current.avgUnitLife.Load() +} + +// GetAvgWorkSlotDuration returns the current average work slot duration. +func (s *Scheduler) GetAvgWorkSlotDuration() int64 { + return s.stats.current.avgWorkSlot.Load() +} + +// GetAvgCatchUpSlotDuration returns the current average catch up slot duration. +func (s *Scheduler) GetAvgCatchUpSlotDuration() int64 { + return s.stats.current.avgCatchUpSlot.Load() +} diff --git a/spn/unit/scheduler_test.go b/spn/unit/scheduler_test.go new file mode 100644 index 00000000..3e3ec6ba --- /dev/null +++ b/spn/unit/scheduler_test.go @@ -0,0 +1,51 @@ +package unit + +import ( + "context" + "testing" +) + +func BenchmarkScheduler(b *testing.B) { + workers := 10 + + // Create and start scheduler. + s := NewScheduler(&SchedulerConfig{}) + ctx, cancel := context.WithCancel(context.Background()) + go func() { + err := s.SlotScheduler(ctx) + if err != nil { + panic(err) + } + }() + defer cancel() + + // Init control structures. + done := make(chan struct{}) + finishedCh := make(chan struct{}) + + // Start workers. + for i := 0; i < workers; i++ { + go func() { + for { + u := s.NewUnit() + u.WaitForSlot() + u.Finish() + select { + case finishedCh <- struct{}{}: + case <-done: + return + } + } + }() + } + + // Start benchmark. + b.ResetTimer() + for i := 0; i < b.N; i++ { + <-finishedCh + } + b.StopTimer() + + // Cleanup. + close(done) +} diff --git a/spn/unit/unit.go b/spn/unit/unit.go new file mode 100644 index 00000000..d198fd64 --- /dev/null +++ b/spn/unit/unit.go @@ -0,0 +1,103 @@ +package unit + +import ( + "time" + + "github.com/tevino/abool" +) + +// Unit describes a "work unit" and is meant to be embedded into another struct +// used for passing data moving through multiple processing steps. +type Unit struct { + id int64 + scheduler *Scheduler + created time.Time + finished abool.AtomicBool + highPriority abool.AtomicBool +} + +// NewUnit returns a new unit within the scheduler. +func (s *Scheduler) NewUnit() *Unit { + return &Unit{ + id: s.currentUnitID.Add(1), + scheduler: s, + created: time.Now(), + } +} + +// ReUse re-initialized the unit to be able to reuse already allocated structs. +func (u *Unit) ReUse() { + // Finish previous unit. + u.Finish() + + // Get new ID and unset finish flag. + u.id = u.scheduler.currentUnitID.Add(1) + u.finished.UnSet() +} + +// WaitForSlot blocks until the unit may be processed. +func (u *Unit) WaitForSlot() { + // High priority units may always process. + if u.highPriority.IsSet() { + return + } + + for { + // Check if we are allowed to process in the current slot. + if u.id <= u.scheduler.clearanceUpTo.Load() { + return + } + + // Debug logging: + // fmt.Printf("unit %d waiting for clearance at %d\n", u.id, u.scheduler.clearanceUpTo.Load()) + + // Wait for next slot. + <-u.scheduler.nextSlotSignal() + } +} + +// Finish signals the unit scheduler that this unit has finished processing. +// Will no-op if called on a nil Unit. +func (u *Unit) Finish() { + if u == nil { + return + } + + // Always increase finished, even if the unit is from a previous epoch. + if u.finished.SetToIf(false, true) { + u.scheduler.finished.Add(1) + + // Record the time this unit took from creation to finish. + timeTaken := time.Since(u.created).Nanoseconds() + u.scheduler.stats.progress.avgUnitLifeCnt.Add(1) + if u.scheduler.stats.progress.avgUnitLifeSum.Add(timeTaken) < 0 { + // Reset if we wrap. + u.scheduler.stats.progress.avgUnitLifeCnt.Store(1) + u.scheduler.stats.progress.avgUnitLifeSum.Store(timeTaken) + } + } +} + +// MakeHighPriority marks the unit as high priority. +func (u *Unit) MakeHighPriority() { + switch { + case u.finished.IsSet(): + // Unit is already finished. + case !u.highPriority.SetToIf(false, true): + // Unit is already set to high priority. + // Else: High Priority set. + case u.id > u.scheduler.clearanceUpTo.Load(): + // Unit is outside current clearance, reduce clearance by one. + u.scheduler.clearanceUpTo.Add(-1) + } +} + +// IsHighPriority returns whether the unit has high priority. +func (u *Unit) IsHighPriority() bool { + return u.highPriority.IsSet() +} + +// RemovePriority removes the high priority mark. +func (u *Unit) RemovePriority() { + u.highPriority.UnSet() +} diff --git a/spn/unit/unit_debug.go b/spn/unit/unit_debug.go new file mode 100644 index 00000000..0ba053bd --- /dev/null +++ b/spn/unit/unit_debug.go @@ -0,0 +1,86 @@ +package unit + +import ( + "sync" + "time" + + "github.com/safing/portbase/log" +) + +// UnitDebugger is used to debug unit leaks. +type UnitDebugger struct { //nolint:golint + units map[int64]*UnitDebugData + unitsLock sync.Mutex +} + +// UnitDebugData represents a unit that is being debugged. +type UnitDebugData struct { //nolint:golint + unit *Unit + unitSource string +} + +// DebugUnit registers the given unit for debug output with the given source. +// Additional calls on the same unit update the unit source. +// StartDebugLog() must be called before calling DebugUnit(). +func (s *Scheduler) DebugUnit(u *Unit, unitSource string) { + // Check if scheduler and unit debugger are created. + if s == nil || s.unitDebugger == nil { + return + } + + s.unitDebugger.unitsLock.Lock() + defer s.unitDebugger.unitsLock.Unlock() + + s.unitDebugger.units[u.id] = &UnitDebugData{ + unit: u, + unitSource: unitSource, + } +} + +// StartDebugLog logs the scheduler state every second. +func (s *Scheduler) StartDebugLog() { + s.unitDebugger = &UnitDebugger{ + units: make(map[int64]*UnitDebugData), + } + + // Force StatCycleDuration to match the debug log output. + s.config.StatCycleDuration = time.Second + + go func() { + for { + s.debugStep() + time.Sleep(time.Second) + } + }() +} + +func (s *Scheduler) debugStep() { + s.unitDebugger.unitsLock.Lock() + defer s.unitDebugger.unitsLock.Unlock() + + // Go through debugging units and clear finished ones, count sources. + sources := make(map[string]int) + for id, debugUnit := range s.unitDebugger.units { + if debugUnit.unit.finished.IsSet() { + delete(s.unitDebugger.units, id) + } else { + cnt := sources[debugUnit.unitSource] + sources[debugUnit.unitSource] = cnt + 1 + } + } + + // Print current state. + log.Debugf( + `scheduler: state: slotPace=%d avgPace=%d maxPace=%d maxLeveledPace=%d currentUnitID=%d clearanceUpTo=%d unitLife=%s slotDurations=%s/%s`, + s.slotPace.Load(), + s.GetAvgSlotPace(), + s.GetMaxSlotPace(), + s.GetMaxLeveledSlotPace(), + s.currentUnitID.Load(), + s.clearanceUpTo.Load(), + time.Duration(s.GetAvgUnitLife()).Round(10*time.Microsecond), + time.Duration(s.GetAvgWorkSlotDuration()).Round(10*time.Microsecond), + time.Duration(s.GetAvgCatchUpSlotDuration()).Round(10*time.Microsecond), + ) + log.Debugf("scheduler: unit sources: %+v", sources) +} diff --git a/spn/unit/unit_test.go b/spn/unit/unit_test.go new file mode 100644 index 00000000..8f5a5ac8 --- /dev/null +++ b/spn/unit/unit_test.go @@ -0,0 +1,104 @@ +package unit + +import ( + "context" + "fmt" + "math" + "math/rand" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestUnit(t *testing.T) { //nolint:paralleltest + // Ignore deprectation, as the given alternative is not safe for concurrent use. + // The global rand methods use a locked seed, which is not available from outside. + rand.Seed(time.Now().UnixNano()) //nolint + + size := 1000000 + workers := 100 + + // Create and start scheduler. + s := NewScheduler(&SchedulerConfig{}) + s.StartDebugLog() + ctx, cancel := context.WithCancel(context.Background()) + go func() { + err := s.SlotScheduler(ctx) + if err != nil { + panic(err) + } + }() + defer cancel() + + // Create 10 workers. + var wg sync.WaitGroup + wg.Add(workers) + sizePerWorker := size / workers + for i := 0; i < workers; i++ { + go func() { + for i := 0; i < sizePerWorker; i++ { + u := s.NewUnit() + + // Make 1% high priority. + if rand.Int()%100 == 0 { //nolint:gosec // This is a test. + u.MakeHighPriority() + } + + u.WaitForSlot() + time.Sleep(10 * time.Microsecond) + u.Finish() + } + wg.Done() + }() + } + + // Wait for workers to finish. + wg.Wait() + + // Wait for two slot durations for values to update. + time.Sleep(s.config.SlotDuration * 2) + + // Print current state. + s.cycleStats() + fmt.Printf(`scheduler state: + currentUnitID = %d + slotPace = %d + clearanceUpTo = %d + finished = %d + maxPace = %d + maxLeveledPace = %d + avgPace = %d + avgUnitLife = %s + avgWorkSlot = %s + avgCatchUpSlot = %s +`, + s.currentUnitID.Load(), + s.slotPace.Load(), + s.clearanceUpTo.Load(), + s.finished.Load(), + s.GetMaxSlotPace(), + s.GetMaxLeveledSlotPace(), + s.GetAvgSlotPace(), + time.Duration(s.GetAvgUnitLife()), + time.Duration(s.GetAvgWorkSlotDuration()), + time.Duration(s.GetAvgCatchUpSlotDuration()), + ) + + // Check if everything seems good. + assert.Equal(t, size, int(s.currentUnitID.Load()), "currentUnitID must match size") + assert.GreaterOrEqual( + t, + int(s.clearanceUpTo.Load()), + size+int(float64(s.config.MinSlotPace)*s.config.SlotChangeRatePerStreak), + "clearanceUpTo must be at least size+minSlotPace", + ) + + // Shutdown + cancel() + time.Sleep(s.config.SlotDuration * 10) + + // Check if scheduler shut down correctly. + assert.Equal(t, math.MaxInt64-math.MaxInt32, int(s.clearanceUpTo.Load()), "clearance must be near MaxInt64") +} From 66381baa1a6649b2588631ba42cc0eecdf07b3dd Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 19 Mar 2024 12:38:19 +0100 Subject: [PATCH 02/35] migrate build system to earthly: supports building core, start and angular for all supported platforms. tauri still missing --- .earthlyignore | 10 ++ Earthfile | 177 ++++++++++++++++++++++++++++++++ cmds/winkext-test/main.go | 13 +-- cmds/winkext-test/main_linux.go | 10 ++ desktop/angular/.gitignore | 4 + 5 files changed, 206 insertions(+), 8 deletions(-) create mode 100644 .earthlyignore create mode 100644 Earthfile create mode 100644 cmds/winkext-test/main_linux.go create mode 100644 desktop/angular/.gitignore diff --git a/.earthlyignore b/.earthlyignore new file mode 100644 index 00000000..9b694cb7 --- /dev/null +++ b/.earthlyignore @@ -0,0 +1,10 @@ +go.work +go.work.sum + +dist/ +node_modules/ + +desktop/angular/node_modules +desktop/angular/dist +desktop/angular/dist-lib +desktop/angular/dist-extension \ No newline at end of file diff --git a/Earthfile b/Earthfile new file mode 100644 index 00000000..1ca151dc --- /dev/null +++ b/Earthfile @@ -0,0 +1,177 @@ +VERSION --arg-scope-and-set 0.8 + +ARG --global go_version = 1.21 +ARG --global distro = alpine3.18 +ARG --global node_version = 18 +ARG --global outputDir = "./dist" + +go-deps: + FROM golang:${go_version}-${distro} + WORKDIR /go-workdir + + # These cache dirs will be used in later test and build targets + # to persist cached go packages. + # + # NOTE: cache only gets persisted on successful builds. A test + # failure will prevent the go cache from being persisted. + ENV GOCACHE = "/.go-cache" + ENV GOMODCACHE = "/.go-mod-cache" + + # Copying only go.mod and go.sum means that the cache for this + # target will only be busted when go.mod/go.sum change. This + # means that we can cache the results of 'go mod download'. + COPY go.mod . + COPY go.sum . + RUN go mod download + + +go-base: + FROM +go-deps + + # Only copy go-code related files to improve caching. + # (i.e. do not rebuild go if only the angular app changed) + COPY cmds ./cmds + COPY runtime ./runtime + COPY service ./service + COPY spn ./spn + +# mod-tidy runs 'go mod tidy', saving go.mod and go.sum locally. +mod-tidy: + FROM +go-base + + RUN go mod tidy + SAVE ARTIFACT go.mod AS LOCAL go.mod + SAVE ARTIFACT --if-exists go.sum AS LOCAL go.sum + +# build-go runs 'go build ./cmds/...', saving artifacts locally. +# If --CMDS is not set, it defaults to building portmaster-start, portmaster-core and hub +build-go: + FROM +go-base + + # Arguments for cross-compilation. + ARG GOOS=linux + ARG GOARCH=amd64 + ARG GOARM + ARG CMDS=portmaster-start portmaster-core hub + + CACHE --sharing shared "$GOCACHE" + CACHE --sharing shared "$GOMODCACHE" + + RUN mkdir /tmp/build + ENV CGO_ENABLED = "0" + + IF [ "${CMDS}" = "" ] + LET CMDS=$(ls -1 "./cmds/") + END + + # Build all go binaries from the specified in CMDS + FOR bin IN $CMDS + RUN go build -o "/tmp/build/" ./cmds/${bin} + END + + LET NAME = "" + + FOR bin IN $(ls -1 "/tmp/build/") + SET NAME = "${outputDir}/${GOOS}_${GOARCH}/${bin}" + IF [ "${GOARM}" != "" ] + SET NAME = "${outputDir}/${GOOS}_${GOARCH}v${GOARM}/${bin}" + END + + SAVE ARTIFACT "/tmp/build/${bin}" AS LOCAL "${NAME}" + END + +# Test one or more go packages. +# Run `earthly +test-go` to test all packages +# Run `earthly +test-go --PKG="service/firewall"` to only test a specific package. +# Run `earthly +test-go --TESTFLAGS="-short"` to add custom flags to go test (-short in this case) +test-go: + FROM +go-base + + ARG GOOS=linux + ARG GOARCH=amd64 + ARG GOARM + ARG TESTFLAGS + ARG PKG="..." + + CACHE --sharing shared "$GOCACHE" + CACHE --sharing shared "$GOMODCACHE" + + FOR pkg IN $(go list -e "./${PKG}") + RUN go test -cover ${TESTFLAGS} ${pkg} + END + +test-go-all-platforms: + # Linux platforms: + BUILD +test-go --GOARCH=amd64 --GOOS=linux + BUILD +test-go --GOARCH=arm64 --GOOS=linux + BUILD +test-go --GOARCH=arm --GOOS=linux --GOARM=5 + BUILD +test-go --GOARCH=arm --GOOS=linux --GOARM=6 + BUILD +test-go --GOARCH=arm --GOOS=linux --GOARM=7 + + # Windows platforms: + BUILD +test-go --GOARCH=amd64 --GOOS=windows + BUILD +test-go --GOARCH=arm64 --GOOS=windows + +# Builds portmaster-start and portmaster-core for all supported platforms +build-go-release: + # Linux platforms: + BUILD +build-go --GOARCH=amd64 --GOOS=linux + BUILD +build-go --GOARCH=arm64 --GOOS=linux + BUILD +build-go --GOARCH=arm --GOOS=linux --GOARM=5 + BUILD +build-go --GOARCH=arm --GOOS=linux --GOARM=6 + BUILD +build-go --GOARCH=arm --GOOS=linux --GOARM=7 + + # Windows platforms: + BUILD +build-go --GOARCH=amd64 --GOOS=windows + BUILD +build-go --GOARCH=arm64 --GOOS=windows + +# Builds all binaries from the cmds/ folder for linux/windows AMD64 +# Most utility binaries are never needed on other platforms. +build-utils: + BUILD +build-go --CMDS="" --GOARCH=amd64 --GOOS=linux + BUILD +build-go --CMDS="" --GOARCH=amd64 --GOOS=windows + +# Prepares the angular project +angular-deps: + FROM node:${node_version} + WORKDIR /app/ui + + RUN apt update && apt install zip + + CACHE --sharing shared "/app/ui/node_modules" + + COPY desktop/angular/package.json . + COPY desktop/angular/package-lock.json . + RUN npm install + + +angular-base: + FROM +angular-deps + + COPY desktop/angular/ . + +# Build the Portmaster UI (angular) in release mode +angular-release: + FROM +angular-base + + CACHE --sharing shared "/app/ui/node_modules" + + RUN npm run build + RUN zip -r ./angular.zip ./dist + SAVE ARTIFACT "./angular.zip" AS LOCAL ${outputDir}/angular.zip + SAVE ARTIFACT "./dist" AS LOCAL ${outputDir}/angular + + +# Build the Portmaster UI (angular) in dev mode +angular-dev: + FROM +angular-base + + CACHE --sharing shared "/app/ui/node_modules" + + RUN npm run build:dev + SAVE ARTIFACT ./dist AS LOCAL ${outputDir}/angular + + +release: + BUILD +build-go-release + BUILD +angular-release diff --git a/cmds/winkext-test/main.go b/cmds/winkext-test/main.go index 0a3d8c4b..9b17b1a3 100644 --- a/cmds/winkext-test/main.go +++ b/cmds/winkext-test/main.go @@ -4,6 +4,7 @@ package main import ( + "context" "flag" "fmt" "os" @@ -75,7 +76,7 @@ func main() { log.Infof("using .sys at %s", sysPath) // init - err = windowskext.Init(dllPath, sysPath) + err = windowskext.Init(sysPath) if err != nil { log.Criticalf("failed to init kext: %s", err) return @@ -89,7 +90,7 @@ func main() { } packets = make(chan packet.Packet, 1000) - go windowskext.Handler(packets) + go windowskext.Handler(context.TODO(), packets) go handlePackets() // catch interrupt for clean shutdown @@ -135,12 +136,8 @@ func handlePackets() { handledPackets++ if getPayload { - data, err := pkt.GetPayload() - if err != nil { - log.Errorf("failed to get payload: %s", err) - } else { - log.Infof("payload is: %x", data) - } + data := pkt.Payload() + log.Infof("payload is: %x", data) } // reroute dns requests to nameserver diff --git a/cmds/winkext-test/main_linux.go b/cmds/winkext-test/main_linux.go new file mode 100644 index 00000000..951e30d2 --- /dev/null +++ b/cmds/winkext-test/main_linux.go @@ -0,0 +1,10 @@ +//go:build linux +// +build linux + +package main + +import "log" + +func main() { + log.Fatalf("winkext-test not supported on linux") +} diff --git a/desktop/angular/.gitignore b/desktop/angular/.gitignore new file mode 100644 index 00000000..28f76669 --- /dev/null +++ b/desktop/angular/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +dist-extension +dist-lib \ No newline at end of file From 4b77945517718f1b3591de96e9ae1d39260639ff Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 20 Mar 2024 10:43:29 +0100 Subject: [PATCH 03/35] Migrate Angular UI from portmaster-ui to desktop/angular. Update Earthfile to build libs, UI and tauri-builtin --- .earthlyignore | 7 +- Earthfile | 63 +- assets/fonts/Roboto-300/LICENSE.txt | 202 + assets/fonts/Roboto-300/Roboto-300.eot | Bin 0 -> 16205 bytes assets/fonts/Roboto-300/Roboto-300.svg | 314 + assets/fonts/Roboto-300/Roboto-300.ttf | Bin 0 -> 32664 bytes assets/fonts/Roboto-300/Roboto-300.woff | Bin 0 -> 13360 bytes assets/fonts/Roboto-300/Roboto-300.woff2 | Bin 0 -> 10324 bytes assets/fonts/Roboto-300italic/LICENSE.txt | 202 + .../Roboto-300italic/Roboto-300italic.eot | Bin 0 -> 17886 bytes .../Roboto-300italic/Roboto-300italic.svg | 327 + .../Roboto-300italic/Roboto-300italic.ttf | Bin 0 -> 34384 bytes .../Roboto-300italic/Roboto-300italic.woff | Bin 0 -> 15004 bytes .../Roboto-300italic/Roboto-300italic.woff2 | Bin 0 -> 11844 bytes assets/fonts/Roboto-500/LICENSE.txt | 202 + assets/fonts/Roboto-500/Roboto-500.eot | Bin 0 -> 16310 bytes assets/fonts/Roboto-500/Roboto-500.svg | 305 + assets/fonts/Roboto-500/Roboto-500.ttf | Bin 0 -> 32580 bytes assets/fonts/Roboto-500/Roboto-500.woff | Bin 0 -> 13248 bytes assets/fonts/Roboto-500/Roboto-500.woff2 | Bin 0 -> 10248 bytes assets/fonts/Roboto-500italic/LICENSE.txt | 202 + .../Roboto-500italic/Roboto-500italic.eot | Bin 0 -> 17584 bytes .../Roboto-500italic/Roboto-500italic.svg | 327 + .../Roboto-500italic/Roboto-500italic.ttf | Bin 0 -> 33868 bytes .../Roboto-500italic/Roboto-500italic.woff | Bin 0 -> 14620 bytes .../Roboto-500italic/Roboto-500italic.woff2 | Bin 0 -> 11532 bytes assets/fonts/Roboto-700/LICENSE.txt | 202 + assets/fonts/Roboto-700/Roboto-700.eot | Bin 0 -> 16208 bytes assets/fonts/Roboto-700/Roboto-700.svg | 310 + assets/fonts/Roboto-700/Roboto-700.ttf | Bin 0 -> 32500 bytes assets/fonts/Roboto-700/Roboto-700.woff | Bin 0 -> 13348 bytes assets/fonts/Roboto-700/Roboto-700.woff2 | Bin 0 -> 10276 bytes assets/fonts/Roboto-700italic/LICENSE.txt | 202 + .../Roboto-700italic/Roboto-700italic.eot | Bin 0 -> 17151 bytes .../Roboto-700italic/Roboto-700italic.svg | 325 + .../Roboto-700italic/Roboto-700italic.ttf | Bin 0 -> 32808 bytes .../Roboto-700italic/Roboto-700italic.woff | Bin 0 -> 14708 bytes .../Roboto-700italic/Roboto-700italic.woff2 | Bin 0 -> 11492 bytes assets/fonts/Roboto-italic/LICENSE.txt | 202 + assets/fonts/Roboto-italic/Roboto-italic.eot | Bin 0 -> 17534 bytes assets/fonts/Roboto-italic/Roboto-italic.svg | 323 + assets/fonts/Roboto-italic/Roboto-italic.ttf | Bin 0 -> 33404 bytes assets/fonts/Roboto-italic/Roboto-italic.woff | Bin 0 -> 14716 bytes .../fonts/Roboto-italic/Roboto-italic.woff2 | Bin 0 -> 11500 bytes assets/fonts/Roboto-regular/LICENSE.txt | 202 + .../fonts/Roboto-regular/Roboto-regular.eot | Bin 0 -> 16227 bytes .../fonts/Roboto-regular/Roboto-regular.svg | 308 + .../fonts/Roboto-regular/Roboto-regular.ttf | Bin 0 -> 32652 bytes .../fonts/Roboto-regular/Roboto-regular.woff | Bin 0 -> 13308 bytes .../fonts/Roboto-regular/Roboto-regular.woff2 | Bin 0 -> 10292 bytes assets/fonts/roboto-slimfix.css | 111 + assets/fonts/roboto.css | 111 + assets/icons/README.md | 3 + assets/icons/pm_dark_128.png | Bin 0 -> 10900 bytes assets/icons/pm_dark_256.png | Bin 0 -> 16639 bytes assets/icons/pm_dark_512.ico | Bin 0 -> 110291 bytes assets/icons/pm_dark_512.png | Bin 0 -> 31400 bytes assets/icons/pm_dark_blue_128.png | Bin 0 -> 10678 bytes assets/icons/pm_dark_blue_256.png | Bin 0 -> 17006 bytes assets/icons/pm_dark_blue_512.ico | Bin 0 -> 99678 bytes assets/icons/pm_dark_blue_512.png | Bin 0 -> 26121 bytes assets/icons/pm_dark_green_128.png | Bin 0 -> 11587 bytes assets/icons/pm_dark_green_256.png | Bin 0 -> 18162 bytes assets/icons/pm_dark_green_512.ico | Bin 0 -> 112077 bytes assets/icons/pm_dark_green_512.png | Bin 0 -> 28931 bytes assets/icons/pm_dark_red_128.png | Bin 0 -> 11443 bytes assets/icons/pm_dark_red_256.png | Bin 0 -> 17932 bytes assets/icons/pm_dark_red_512.ico | Bin 0 -> 112150 bytes assets/icons/pm_dark_red_512.png | Bin 0 -> 29114 bytes assets/icons/pm_dark_yellow_128.png | Bin 0 -> 11569 bytes assets/icons/pm_dark_yellow_256.png | Bin 0 -> 18137 bytes assets/icons/pm_dark_yellow_512.ico | Bin 0 -> 112046 bytes assets/icons/pm_dark_yellow_512.png | Bin 0 -> 28968 bytes assets/icons/pm_light_128.png | Bin 0 -> 11328 bytes assets/icons/pm_light_256.png | Bin 0 -> 17563 bytes assets/icons/pm_light_512.ico | Bin 0 -> 111080 bytes assets/icons/pm_light_512.png | Bin 0 -> 31361 bytes assets/icons/pm_light_blue_128.png | Bin 0 -> 10888 bytes assets/icons/pm_light_blue_256.png | Bin 0 -> 15813 bytes assets/icons/pm_light_blue_512.ico | Bin 0 -> 113172 bytes assets/icons/pm_light_blue_512.png | Bin 0 -> 25466 bytes assets/icons/pm_light_green_128.png | Bin 0 -> 11555 bytes assets/icons/pm_light_green_256.png | Bin 0 -> 18828 bytes assets/icons/pm_light_green_512.ico | Bin 0 -> 113175 bytes assets/icons/pm_light_green_512.png | Bin 0 -> 28929 bytes assets/icons/pm_light_red_128.png | Bin 0 -> 11625 bytes assets/icons/pm_light_red_256.png | Bin 0 -> 18969 bytes assets/icons/pm_light_red_512.ico | Bin 0 -> 113172 bytes assets/icons/pm_light_red_512.png | Bin 0 -> 28952 bytes assets/icons/pm_light_yellow_128.png | Bin 0 -> 11555 bytes assets/icons/pm_light_yellow_256.png | Bin 0 -> 18756 bytes assets/icons/pm_light_yellow_512.ico | Bin 0 -> 113171 bytes assets/icons/pm_light_yellow_512.png | Bin 0 -> 29002 bytes assets/img/Mobile.svg | 1 + assets/img/flags/AD.png | Bin 0 -> 263 bytes assets/img/flags/AE.png | Bin 0 -> 107 bytes assets/img/flags/AF.png | Bin 0 -> 259 bytes assets/img/flags/AG.png | Bin 0 -> 302 bytes assets/img/flags/AI.png | Bin 0 -> 332 bytes assets/img/flags/AL.png | Bin 0 -> 291 bytes assets/img/flags/AM.png | Bin 0 -> 105 bytes assets/img/flags/AN.png | Bin 0 -> 145 bytes assets/img/flags/AO.png | Bin 0 -> 241 bytes assets/img/flags/AQ.png | Bin 0 -> 382 bytes assets/img/flags/AR.png | Bin 0 -> 209 bytes assets/img/flags/AS.png | Bin 0 -> 448 bytes assets/img/flags/AT.png | Bin 0 -> 98 bytes assets/img/flags/AU.png | Bin 0 -> 228 bytes assets/img/flags/AW.png | Bin 0 -> 182 bytes assets/img/flags/AX.png | Bin 0 -> 121 bytes assets/img/flags/AZ.png | Bin 0 -> 267 bytes assets/img/flags/BA.png | Bin 0 -> 355 bytes assets/img/flags/BB.png | Bin 0 -> 159 bytes assets/img/flags/BD.png | Bin 0 -> 211 bytes assets/img/flags/BE.png | Bin 0 -> 102 bytes assets/img/flags/BF.png | Bin 0 -> 166 bytes assets/img/flags/BG.png | Bin 0 -> 103 bytes assets/img/flags/BH.png | Bin 0 -> 129 bytes assets/img/flags/BI.png | Bin 0 -> 454 bytes assets/img/flags/BJ.png | Bin 0 -> 106 bytes assets/img/flags/BL.png | Bin 0 -> 539 bytes assets/img/flags/BM.png | Bin 0 -> 321 bytes assets/img/flags/BN.png | Bin 0 -> 518 bytes assets/img/flags/BO.png | Bin 0 -> 236 bytes assets/img/flags/BR.png | Bin 0 -> 432 bytes assets/img/flags/BS.png | Bin 0 -> 171 bytes assets/img/flags/BT.png | Bin 0 -> 449 bytes assets/img/flags/BW.png | Bin 0 -> 108 bytes assets/img/flags/BY.png | Bin 0 -> 151 bytes assets/img/flags/BZ.png | Bin 0 -> 337 bytes assets/img/flags/CA.png | Bin 0 -> 177 bytes assets/img/flags/CC.png | Bin 0 -> 259 bytes assets/img/flags/CD.png | Bin 0 -> 432 bytes assets/img/flags/CF.png | Bin 0 -> 162 bytes assets/img/flags/CG.png | Bin 0 -> 152 bytes assets/img/flags/CH.png | Bin 0 -> 100 bytes assets/img/flags/CI.png | Bin 0 -> 100 bytes assets/img/flags/CK.png | Bin 0 -> 331 bytes assets/img/flags/CL.png | Bin 0 -> 150 bytes assets/img/flags/CM.png | Bin 0 -> 163 bytes assets/img/flags/CN.png | Bin 0 -> 310 bytes assets/img/flags/CO.png | Bin 0 -> 108 bytes assets/img/flags/CR.png | Bin 0 -> 110 bytes assets/img/flags/CT.png | Bin 0 -> 1356 bytes assets/img/flags/CU.png | Bin 0 -> 215 bytes assets/img/flags/CV.png | Bin 0 -> 138 bytes assets/img/flags/CW.png | Bin 0 -> 191 bytes assets/img/flags/CX.png | Bin 0 -> 390 bytes assets/img/flags/CY.png | Bin 0 -> 364 bytes assets/img/flags/CZ.png | Bin 0 -> 221 bytes assets/img/flags/DE.png | Bin 0 -> 102 bytes assets/img/flags/DJ.png | Bin 0 -> 228 bytes assets/img/flags/DK.png | Bin 0 -> 106 bytes assets/img/flags/DM.png | Bin 0 -> 333 bytes assets/img/flags/DO.png | Bin 0 -> 142 bytes assets/img/flags/DZ.png | Bin 0 -> 309 bytes assets/img/flags/EC.png | Bin 0 -> 264 bytes assets/img/flags/EE.png | Bin 0 -> 102 bytes assets/img/flags/EG.png | Bin 0 -> 199 bytes assets/img/flags/EH.png | Bin 0 -> 248 bytes assets/img/flags/ER.png | Bin 0 -> 421 bytes assets/img/flags/ES.png | Bin 0 -> 221 bytes assets/img/flags/ET.png | Bin 0 -> 420 bytes assets/img/flags/EU.png | Bin 0 -> 316 bytes assets/img/flags/FI.png | Bin 0 -> 103 bytes assets/img/flags/FJ.png | Bin 0 -> 387 bytes assets/img/flags/FK.png | Bin 0 -> 344 bytes assets/img/flags/FM.png | Bin 0 -> 198 bytes assets/img/flags/FO.png | Bin 0 -> 122 bytes assets/img/flags/FR.png | Bin 0 -> 100 bytes assets/img/flags/GA.png | Bin 0 -> 108 bytes assets/img/flags/GB.png | Bin 0 -> 353 bytes assets/img/flags/GD.png | Bin 0 -> 313 bytes assets/img/flags/GE.png | Bin 0 -> 122 bytes assets/img/flags/GG.png | Bin 0 -> 124 bytes assets/img/flags/GH.png | Bin 0 -> 162 bytes assets/img/flags/GI.png | Bin 0 -> 245 bytes assets/img/flags/GL.png | Bin 0 -> 196 bytes assets/img/flags/GM.png | Bin 0 -> 115 bytes assets/img/flags/GN.png | Bin 0 -> 103 bytes assets/img/flags/GQ.png | Bin 0 -> 308 bytes assets/img/flags/GR.png | Bin 0 -> 141 bytes assets/img/flags/GS.png | Bin 0 -> 455 bytes assets/img/flags/GT.png | Bin 0 -> 198 bytes assets/img/flags/GU.png | Bin 0 -> 228 bytes assets/img/flags/GW.png | Bin 0 -> 149 bytes assets/img/flags/GY.png | Bin 0 -> 393 bytes assets/img/flags/HK.png | Bin 0 -> 418 bytes assets/img/flags/HN.png | Bin 0 -> 154 bytes assets/img/flags/HR.png | Bin 0 -> 391 bytes assets/img/flags/HT.png | Bin 0 -> 206 bytes assets/img/flags/HU.png | Bin 0 -> 104 bytes assets/img/flags/IC.png | Bin 0 -> 183 bytes assets/img/flags/ID.png | Bin 0 -> 98 bytes assets/img/flags/IE.png | Bin 0 -> 99 bytes assets/img/flags/IL.png | Bin 0 -> 180 bytes assets/img/flags/IM.png | Bin 0 -> 367 bytes assets/img/flags/IN.png | Bin 0 -> 194 bytes assets/img/flags/IQ.png | Bin 0 -> 269 bytes assets/img/flags/IR.png | Bin 0 -> 356 bytes assets/img/flags/IS.png | Bin 0 -> 124 bytes assets/img/flags/IT.png | Bin 0 -> 100 bytes assets/img/flags/JE.png | Bin 0 -> 403 bytes assets/img/flags/JM.png | Bin 0 -> 392 bytes assets/img/flags/JO.png | Bin 0 -> 236 bytes assets/img/flags/JP.png | Bin 0 -> 155 bytes assets/img/flags/KE.png | Bin 0 -> 324 bytes assets/img/flags/KG.png | Bin 0 -> 380 bytes assets/img/flags/KH.png | Bin 0 -> 232 bytes assets/img/flags/KI.png | Bin 0 -> 517 bytes assets/img/flags/KM.png | Bin 0 -> 272 bytes assets/img/flags/KN.png | Bin 0 -> 403 bytes assets/img/flags/KP.png | Bin 0 -> 197 bytes assets/img/flags/KR.png | Bin 0 -> 413 bytes assets/img/flags/KW.png | Bin 0 -> 185 bytes assets/img/flags/KY.png | Bin 0 -> 338 bytes assets/img/flags/KZ.png | Bin 0 -> 405 bytes assets/img/flags/LA.png | Bin 0 -> 175 bytes assets/img/flags/LB.png | Bin 0 -> 213 bytes assets/img/flags/LC.png | Bin 0 -> 197 bytes assets/img/flags/LI.png | Bin 0 -> 216 bytes assets/img/flags/LICENSE.txt | 7 + assets/img/flags/LK.png | Bin 0 -> 325 bytes assets/img/flags/LR.png | Bin 0 -> 142 bytes assets/img/flags/LS.png | Bin 0 -> 200 bytes assets/img/flags/LT.png | Bin 0 -> 108 bytes assets/img/flags/LU.png | Bin 0 -> 105 bytes assets/img/flags/LV.png | Bin 0 -> 99 bytes assets/img/flags/LY.png | Bin 0 -> 212 bytes assets/img/flags/MA.png | Bin 0 -> 302 bytes assets/img/flags/MC.png | Bin 0 -> 98 bytes assets/img/flags/MD.png | Bin 0 -> 190 bytes assets/img/flags/ME.png | Bin 0 -> 323 bytes assets/img/flags/MF.png | Bin 0 -> 161 bytes assets/img/flags/MG.png | Bin 0 -> 101 bytes assets/img/flags/MH.png | Bin 0 -> 382 bytes assets/img/flags/MK.png | Bin 0 -> 378 bytes assets/img/flags/ML.png | Bin 0 -> 103 bytes assets/img/flags/MM.png | Bin 0 -> 195 bytes assets/img/flags/MN.png | Bin 0 -> 225 bytes assets/img/flags/MO.png | Bin 0 -> 413 bytes assets/img/flags/MP.png | Bin 0 -> 548 bytes assets/img/flags/MQ.png | Bin 0 -> 202 bytes assets/img/flags/MR.png | Bin 0 -> 250 bytes assets/img/flags/MS.png | Bin 0 -> 346 bytes assets/img/flags/MT.png | Bin 0 -> 114 bytes assets/img/flags/MU.png | Bin 0 -> 116 bytes assets/img/flags/MV.png | Bin 0 -> 201 bytes assets/img/flags/MW.png | Bin 0 -> 193 bytes assets/img/flags/MX.png | Bin 0 -> 207 bytes assets/img/flags/MY.png | Bin 0 -> 236 bytes assets/img/flags/MZ.png | Bin 0 -> 315 bytes assets/img/flags/NA.png | Bin 0 -> 452 bytes assets/img/flags/NC.png | Bin 0 -> 325 bytes assets/img/flags/NE.png | Bin 0 -> 153 bytes assets/img/flags/NF.png | Bin 0 -> 295 bytes assets/img/flags/NG.png | Bin 0 -> 98 bytes assets/img/flags/NI.png | Bin 0 -> 210 bytes assets/img/flags/NL.png | Bin 0 -> 104 bytes assets/img/flags/NO.png | Bin 0 -> 124 bytes assets/img/flags/NP.png | Bin 0 -> 241 bytes assets/img/flags/NR.png | Bin 0 -> 172 bytes assets/img/flags/NU.png | Bin 0 -> 252 bytes assets/img/flags/NZ.png | Bin 0 -> 200 bytes assets/img/flags/OM.png | Bin 0 -> 198 bytes assets/img/flags/PA.png | Bin 0 -> 174 bytes assets/img/flags/PE.png | Bin 0 -> 98 bytes assets/img/flags/PF.png | Bin 0 -> 217 bytes assets/img/flags/PG.png | Bin 0 -> 444 bytes assets/img/flags/PH.png | Bin 0 -> 342 bytes assets/img/flags/PK.png | Bin 0 -> 306 bytes assets/img/flags/PL.png | Bin 0 -> 102 bytes assets/img/flags/PN.png | Bin 0 -> 423 bytes assets/img/flags/PR.png | Bin 0 -> 216 bytes assets/img/flags/PS.png | Bin 0 -> 157 bytes assets/img/flags/PT.png | Bin 0 -> 303 bytes assets/img/flags/PW.png | Bin 0 -> 209 bytes assets/img/flags/PY.png | Bin 0 -> 197 bytes assets/img/flags/QA.png | Bin 0 -> 190 bytes assets/img/flags/RE.png | Bin 0 -> 443 bytes assets/img/flags/RO.png | Bin 0 -> 103 bytes assets/img/flags/RS.png | Bin 0 -> 331 bytes assets/img/flags/RU.png | Bin 0 -> 98 bytes assets/img/flags/RW.png | Bin 0 -> 182 bytes assets/img/flags/SA.png | Bin 0 -> 426 bytes assets/img/flags/SB.png | Bin 0 -> 306 bytes assets/img/flags/SC.png | Bin 0 -> 314 bytes assets/img/flags/SD.png | Bin 0 -> 156 bytes assets/img/flags/SE.png | Bin 0 -> 109 bytes assets/img/flags/SG.png | Bin 0 -> 253 bytes assets/img/flags/SH.png | Bin 0 -> 333 bytes assets/img/flags/SI.png | Bin 0 -> 177 bytes assets/img/flags/SK.png | Bin 0 -> 225 bytes assets/img/flags/SL.png | Bin 0 -> 104 bytes assets/img/flags/SM.png | Bin 0 -> 291 bytes assets/img/flags/SN.png | Bin 0 -> 160 bytes assets/img/flags/SO.png | Bin 0 -> 192 bytes assets/img/flags/SR.png | Bin 0 -> 166 bytes assets/img/flags/SS.png | Bin 0 -> 289 bytes assets/img/flags/ST.png | Bin 0 -> 243 bytes assets/img/flags/SV.png | Bin 0 -> 209 bytes assets/img/flags/SX.png | Bin 0 -> 483 bytes assets/img/flags/SY.png | Bin 0 -> 161 bytes assets/img/flags/SZ.png | Bin 0 -> 366 bytes assets/img/flags/TC.png | Bin 0 -> 312 bytes assets/img/flags/TD.png | Bin 0 -> 103 bytes assets/img/flags/TF.png | Bin 0 -> 224 bytes assets/img/flags/TG.png | Bin 0 -> 174 bytes assets/img/flags/TH.png | Bin 0 -> 110 bytes assets/img/flags/TJ.png | Bin 0 -> 203 bytes assets/img/flags/TK.png | Bin 0 -> 260 bytes assets/img/flags/TL.png | Bin 0 -> 277 bytes assets/img/flags/TM.png | Bin 0 -> 392 bytes assets/img/flags/TN.png | Bin 0 -> 271 bytes assets/img/flags/TO.png | Bin 0 -> 114 bytes assets/img/flags/TR.png | Bin 0 -> 311 bytes assets/img/flags/TT.png | Bin 0 -> 358 bytes assets/img/flags/TV.png | Bin 0 -> 398 bytes assets/img/flags/TW.png | Bin 0 -> 205 bytes assets/img/flags/TZ.png | Bin 0 -> 415 bytes assets/img/flags/UA.png | Bin 0 -> 102 bytes assets/img/flags/UG.png | Bin 0 -> 188 bytes assets/img/flags/US.png | Bin 0 -> 120 bytes assets/img/flags/UY.png | Bin 0 -> 216 bytes assets/img/flags/UZ.png | Bin 0 -> 163 bytes assets/img/flags/VA.png | Bin 0 -> 202 bytes assets/img/flags/VC.png | Bin 0 -> 217 bytes assets/img/flags/VE.png | Bin 0 -> 302 bytes assets/img/flags/VG.png | Bin 0 -> 337 bytes assets/img/flags/VI.png | Bin 0 -> 500 bytes assets/img/flags/VN.png | Bin 0 -> 193 bytes assets/img/flags/VU.png | Bin 0 -> 302 bytes assets/img/flags/WF.png | Bin 0 -> 182 bytes assets/img/flags/WS.png | Bin 0 -> 236 bytes assets/img/flags/YE.png | Bin 0 -> 103 bytes assets/img/flags/YT.png | Bin 0 -> 482 bytes assets/img/flags/ZA.png | Bin 0 -> 348 bytes assets/img/flags/ZM.png | Bin 0 -> 189 bytes assets/img/flags/ZW.png | Bin 0 -> 300 bytes assets/img/flags/_abkhazia.png | Bin 0 -> 276 bytes assets/img/flags/_basque-country.png | Bin 0 -> 240 bytes .../flags/_british-antarctic-territory.png | Bin 0 -> 361 bytes assets/img/flags/_commonwealth.png | Bin 0 -> 443 bytes assets/img/flags/_england.png | Bin 0 -> 102 bytes assets/img/flags/_gosquared.png | Bin 0 -> 239 bytes assets/img/flags/_kosovo.png | Bin 0 -> 434 bytes assets/img/flags/_mars.png | Bin 0 -> 103 bytes assets/img/flags/_nagorno-karabakh.png | Bin 0 -> 141 bytes assets/img/flags/_nato.png | Bin 0 -> 143 bytes assets/img/flags/_northern-cyprus.png | Bin 0 -> 220 bytes assets/img/flags/_olympics.png | Bin 0 -> 329 bytes assets/img/flags/_red-cross.png | Bin 0 -> 109 bytes assets/img/flags/_scotland.png | Bin 0 -> 351 bytes assets/img/flags/_somaliland.png | Bin 0 -> 315 bytes assets/img/flags/_south-ossetia.png | Bin 0 -> 100 bytes assets/img/flags/_united-nations.png | Bin 0 -> 366 bytes assets/img/flags/_unknown.png | Bin 0 -> 176 bytes assets/img/flags/_wales.png | Bin 0 -> 527 bytes assets/img/linux.svg | 1 + assets/img/mac.svg | 1 + assets/img/plants1-br.png | Bin 0 -> 25340 bytes assets/img/plants1.png | Bin 0 -> 36805 bytes .../access-regional-content-easily.png | Bin 0 -> 80815 bytes .../built-from-the-ground-up.png | Bin 0 -> 281930 bytes .../img/spn-feature-carousel/bye-bye-vpns.png | Bin 0 -> 103251 bytes .../easily-control-your-privacy.png | Bin 0 -> 72739 bytes .../multiple-identities-for-each-app.png | Bin 0 -> 49252 bytes assets/img/spn-login.png | Bin 0 -> 89031 bytes assets/img/windows.svg | 1 + assets/world-50m.json | 1 + desktop/angular/.gitignore | 3 +- desktop/angular/README.md | 104 + desktop/angular/angular.json | 457 + desktop/angular/assets | 1 + .../angular/browser-extension-dev.config.ts | 16 + desktop/angular/browser-extension.config.ts | 5 + desktop/angular/docker.sh | 18 + desktop/angular/e2e/protractor.conf.js | 36 + desktop/angular/e2e/src/app.e2e-spec.ts | 23 + desktop/angular/e2e/src/app.po.ts | 11 + desktop/angular/e2e/tsconfig.json | 14 + desktop/angular/karma.conf.js | 32 + desktop/angular/package-lock.json | 34959 ++++++++++++++++ desktop/angular/package.json | 105 + .../portmaster-chrome-extension/karma.conf.js | 44 + .../src/app/app-routing.module.ts | 15 + .../src/app/app.component.html | 3 + .../src/app/app.component.scss | 3 + .../src/app/app.component.ts | 54 + .../src/app/app.module.ts | 39 + .../domain-list/domain-list.component.html | 27 + .../app/domain-list/domain-list.component.ts | 129 + .../src/app/domain-list/index.ts | 1 + .../src/app/header/header.component.html | 22 + .../src/app/header/header.component.scss | 29 + .../src/app/header/header.component.ts | 9 + .../src/app/header/index.ts | 1 + .../src/app/interceptor.ts | 45 + .../src/app/request-interceptor.service.ts | 49 + .../src/app/welcome/index.ts | 2 + .../src/app/welcome/intro.component.html | 48 + .../src/app/welcome/intro.component.ts | 44 + .../src/app/welcome/welcome.module.ts | 19 + .../src/assets}/.gitkeep | 0 .../src/assets/icon_128.png | Bin 0 -> 11328 bytes .../src/background.ts | 133 + .../src/background/commands.ts | 14 + .../src/background/tab-tracker.ts | 126 + .../src/background/tab-utils.ts | 9 + .../src/environments/environment.prod.ts | 3 + .../src/environments/environment.ts | 16 + .../src/favicon.ico | Bin 0 -> 948 bytes .../src/index.html | 13 + .../portmaster-chrome-extension/src/main.ts | 12 + .../src/manifest.json | 23 + .../src/polyfills.ts | 53 + .../src/styles.scss | 8 + .../portmaster-chrome-extension/src/test.ts | 14 + .../tsconfig.app.json | 18 + .../tsconfig.spec.json | 18 + .../projects/safing/portmaster-api/README.md | 24 + .../safing/portmaster-api/karma.conf.js | 44 + .../safing/portmaster-api/ng-package.json | 7 + .../safing/portmaster-api/package-lock.json | 132 + .../safing/portmaster-api/package.json | 14 + .../src/lib/app-profile.service.ts | 262 + .../src/lib/app-profile.types.ts | 215 + .../portmaster-api/src/lib/config.service.ts | 128 + .../portmaster-api/src/lib/config.types.ts | 348 + .../portmaster-api/src/lib/core.types.ts | 34 + .../src/lib/debug-api.service.ts | 54 + .../safing/portmaster-api/src/lib/features.ts | 8 + .../src/lib/meta-api.service.ts | 106 + .../safing/portmaster-api/src/lib/module.ts | 55 + .../src/lib/netquery.service.ts | 543 + .../portmaster-api/src/lib/network.types.ts | 314 + .../portmaster-api/src/lib/portapi.service.ts | 1011 + .../portmaster-api/src/lib/portapi.types.ts | 453 + .../portmaster-api/src/lib/spn.service.ts | 171 + .../portmaster-api/src/lib/spn.types.ts | 104 + .../safing/portmaster-api/src/lib/utils.ts | 13 + .../src/lib/websocket.service.ts | 17 + .../safing/portmaster-api/src/public-api.ts | 22 + .../safing/portmaster-api/src/test.ts | 15 + .../safing/portmaster-api/tsconfig.lib.json | 16 + .../portmaster-api/tsconfig.lib.prod.json | 7 + .../safing/portmaster-api/tsconfig.spec.json | 18 + .../angular/projects/safing/ui/.eslintrc.json | 44 + desktop/angular/projects/safing/ui/README.md | 24 + .../angular/projects/safing/ui/karma.conf.js | 44 + .../projects/safing/ui/ng-package.json | 11 + .../angular/projects/safing/ui/package.json | 17 + .../ui/src/lib/accordion/accordion-group.html | 1 + .../ui/src/lib/accordion/accordion-group.ts | 116 + .../ui/src/lib/accordion/accordion.html | 10 + .../ui/src/lib/accordion/accordion.module.ts | 19 + .../safing/ui/src/lib/accordion/accordion.ts | 88 + .../safing/ui/src/lib/accordion/index.ts | 4 + .../safing/ui/src/lib/animations/index.ts | 88 + .../ui/src/lib/dialog/_confirm.dialog.scss | 95 + .../safing/ui/src/lib/dialog/_dialog.scss | 28 + .../ui/src/lib/dialog/confirm.dialog.html | 22 + .../ui/src/lib/dialog/confirm.dialog.ts | 40 + .../ui/src/lib/dialog/dialog.animations.ts | 19 + .../ui/src/lib/dialog/dialog.container.ts | 76 + .../safing/ui/src/lib/dialog/dialog.module.ts | 23 + .../safing/ui/src/lib/dialog/dialog.ref.ts | 62 + .../ui/src/lib/dialog/dialog.service.ts | 154 + .../safing/ui/src/lib/dialog/index.ts | 5 + .../safing/ui/src/lib/dropdown/dropdown.html | 27 + .../ui/src/lib/dropdown/dropdown.module.ts | 18 + .../safing/ui/src/lib/dropdown/dropdown.ts | 216 + .../safing/ui/src/lib/dropdown/index.ts | 3 + .../ui/src/lib/overlay-stepper/index.ts | 5 + .../overlay-stepper-container.html | 22 + .../overlay-stepper-container.ts | 261 + .../overlay-stepper/overlay-stepper.module.ts | 21 + .../lib/overlay-stepper/overlay-stepper.ts | 57 + .../safing/ui/src/lib/overlay-stepper/refs.ts | 143 + .../ui/src/lib/overlay-stepper/step-outlet.ts | 90 + .../safing/ui/src/lib/overlay-stepper/step.ts | 64 + .../ui/src/lib/pagination/_pagination.scss | 22 + .../lib/pagination/dynamic-items-paginator.ts | 64 + .../safing/ui/src/lib/pagination/index.ts | 5 + .../ui/src/lib/pagination/pagination.html | 33 + .../src/lib/pagination/pagination.module.ts | 19 + .../ui/src/lib/pagination/pagination.ts | 132 + .../src/lib/pagination/snapshot-paginator.ts | 64 + .../safing/ui/src/lib/select/_select.scss | 73 + .../safing/ui/src/lib/select/index.ts | 4 + .../projects/safing/ui/src/lib/select/item.ts | 64 + .../safing/ui/src/lib/select/select.html | 88 + .../safing/ui/src/lib/select/select.module.ts | 31 + .../safing/ui/src/lib/select/select.ts | 495 + .../safing/ui/src/lib/tabs/_tab-group.scss | 3 + .../projects/safing/ui/src/lib/tabs/index.ts | 4 + .../safing/ui/src/lib/tabs/tab-group.html | 24 + .../safing/ui/src/lib/tabs/tab-group.ts | 352 + .../projects/safing/ui/src/lib/tabs/tab.ts | 167 + .../safing/ui/src/lib/tabs/tabs.module.ts | 28 + .../safing/ui/src/lib/tipup/_tipup.scss | 52 + .../safing/ui/src/lib/tipup/anchor.ts | 43 + .../safing/ui/src/lib/tipup/clone-node.ts | 128 + .../safing/ui/src/lib/tipup/css-utils.ts | 87 + .../projects/safing/ui/src/lib/tipup/index.ts | 6 + .../safing/ui/src/lib/tipup/safe.pipe.ts | 21 + .../ui/src/lib/tipup/tipup-component.ts | 67 + .../safing/ui/src/lib/tipup/tipup.html | 22 + .../safing/ui/src/lib/tipup/tipup.module.ts | 47 + .../projects/safing/ui/src/lib/tipup/tipup.ts | 526 + .../safing/ui/src/lib/tipup/translations.ts | 27 + .../projects/safing/ui/src/lib/tipup/utils.ts | 8 + .../src/lib/toggle-switch/_toggle-switch.scss | 35 + .../safing/ui/src/lib/toggle-switch/index.ts | 3 + .../src/lib/toggle-switch/toggle-switch.html | 20 + .../ui/src/lib/toggle-switch/toggle-switch.ts | 59 + .../ui/src/lib/toggle-switch/toggle.module.ts | 18 + .../src/lib/tooltip/_tooltip-component.scss | 5 + .../safing/ui/src/lib/tooltip/index.ts | 3 + .../ui/src/lib/tooltip/tooltip-component.html | 6 + .../ui/src/lib/tooltip/tooltip-component.ts | 139 + .../ui/src/lib/tooltip/tooltip.module.ts | 23 + .../safing/ui/src/lib/tooltip/tooltip.ts | 244 + .../projects/safing/ui/src/lib/ui.module.ts | 10 + .../projects/safing/ui/src/public-api.ts | 16 + .../angular/projects/safing/ui/src/test.ts | 16 + .../angular/projects/safing/ui/theming.scss | 8 + .../projects/safing/ui/tsconfig.lib.json | 18 + .../projects/safing/ui/tsconfig.lib.prod.json | 7 + .../projects/safing/ui/tsconfig.spec.json | 17 + .../tauri-builtin/src/app/app.component.html | 105 + .../tauri-builtin/src/app/app.component.ts | 52 + .../tauri-builtin/src/app/app.config.ts | 12 + .../angular/projects/tauri-builtin/src/assets | 1 + .../projects/tauri-builtin/src/favicon.ico | Bin 0 -> 948 bytes .../projects/tauri-builtin/src/index.html | 13 + .../projects/tauri-builtin/src/main.ts | 6 + .../projects/tauri-builtin/src/styles.scss | 7 + .../projects/tauri-builtin/tsconfig.app.json | 10 + desktop/angular/proxy.json | 6 + desktop/angular/src/app/app-routing.module.ts | 68 + desktop/angular/src/app/app.component.html | 53 + desktop/angular/src/app/app.component.scss | 114 + desktop/angular/src/app/app.component.spec.ts | 28 + desktop/angular/src/app/app.component.ts | 234 + desktop/angular/src/app/app.module.ts | 240 + .../angular/src/app/integration/browser.ts | 41 + .../angular/src/app/integration/electron.ts | 55 + .../angular/src/app/integration/factory.ts | 22 + desktop/angular/src/app/integration/index.ts | 2 + .../src/app/integration/integration.ts | 41 + .../angular/src/app/integration/taur-app.ts | 216 + desktop/angular/src/app/intro/index.ts | 1 + desktop/angular/src/app/intro/intro.module.ts | 36 + .../src/app/intro/step-1-welcome/index.ts | 1 + .../intro/step-1-welcome/step-1-welcome.html | 14 + .../intro/step-1-welcome/step-1-welcome.ts | 22 + .../src/app/intro/step-2-trackers/index.ts | 1 + .../step-2-trackers/step-2-trackers.html | 11 + .../intro/step-2-trackers/step-2-trackers.ts | 48 + .../angular/src/app/intro/step-3-dns/index.ts | 1 + .../src/app/intro/step-3-dns/step-3-dns.html | 17 + .../src/app/intro/step-3-dns/step-3-dns.ts | 106 + .../src/app/intro/step-4-tipups/index.ts | 1 + .../intro/step-4-tipups/step-4-tipups.html | 11 + .../app/intro/step-4-tipups/step-4-tipups.ts | 12 + desktop/angular/src/app/intro/step.scss | 11 + .../src/app/layout/navigation/navigation.html | 230 + .../src/app/layout/navigation/navigation.scss | 98 + .../src/app/layout/navigation/navigation.ts | 298 + .../src/app/layout/side-dash/side-dash.html | 10 + .../src/app/layout/side-dash/side-dash.scss | 11 + .../src/app/layout/side-dash/side-dash.ts | 13 + desktop/angular/src/app/package-lock.json | 27 + desktop/angular/src/app/package.json | 12 + .../app-insights/app-insights.component.html | 13 + .../app-insights/app-insights.component.ts | 96 + .../src/app/pages/app-view/app-view.html | 425 + .../src/app/pages/app-view/app-view.scss | 3 + .../src/app/pages/app-view/app-view.ts | 641 + .../angular/src/app/pages/app-view/index.ts | 3 + .../merge-profile-dialog.component.html | 36 + .../merge-profile-dialog.component.ts | 62 + .../src/app/pages/app-view/overview.html | 193 + .../src/app/pages/app-view/overview.scss | 54 + .../src/app/pages/app-view/overview.ts | 305 + .../qs-history/qs-history.component.html | 12 + .../qs-history/qs-history.component.scss} | 0 .../qs-history/qs-history.component.ts | 67 + .../app/pages/app-view/qs-internet/index.ts | 1 + .../app-view/qs-internet/qs-internet.html | 30 + .../pages/app-view/qs-internet/qs-internet.ts | 79 + .../pages/app-view/qs-select-exit/index.ts | 1 + .../qs-select-exit/qs-select-exit.html | 39 + .../qs-select-exit/qs-select-exit.scss | 0 .../app-view/qs-select-exit/qs-select-exit.ts | 128 + .../app/pages/app-view/qs-use-spn/index.ts | 1 + .../pages/app-view/qs-use-spn/qs-use-spn.html | 42 + .../pages/app-view/qs-use-spn/qs-use-spn.ts | 97 + .../dashboard-widget.component.html | 14 + .../dashboard-widget.component.ts | 30 + .../pages/dashboard/dashboard.component.html | 281 + .../pages/dashboard/dashboard.component.scss | 166 + .../pages/dashboard/dashboard.component.ts | 481 + .../feature-card/feature-card.component.html | 61 + .../feature-card/feature-card.component.scss | 60 + .../feature-card/feature-card.component.ts | 128 + .../angular/src/app/pages/monitor/index.ts | 1 + .../src/app/pages/monitor/monitor.html | 46 + .../src/app/pages/monitor/monitor.scss | 49 + .../angular/src/app/pages/monitor/monitor.ts | 77 + desktop/angular/src/app/pages/page.scss | 6 + .../src/app/pages/settings/settings.html | 26 + .../src/app/pages/settings/settings.scss | 83 + .../src/app/pages/settings/settings.ts | 133 + .../spn/country-details/country-details.html | 154 + .../spn/country-details/country-details.ts | 217 + .../app/pages/spn/country-details/index.ts | 1 + .../spn/country-overlay/country-overlay.html | 25 + .../spn/country-overlay/country-overlay.scss | 40 + .../spn/country-overlay/country-overlay.ts | 75 + .../app/pages/spn/country-overlay/index.ts | 1 + desktop/angular/src/app/pages/spn/index.ts | 1 + .../src/app/pages/spn/map-legend/index.ts | 1 + .../app/pages/spn/map-legend/map-legend.html | 54 + .../app/pages/spn/map-legend/map-legend.ts | 69 + .../src/app/pages/spn/map-renderer/index.ts | 1 + .../pages/spn/map-renderer/map-renderer.ts | 383 + .../app/pages/spn/map-renderer/map-style.scss | 167 + .../angular/src/app/pages/spn/map.service.ts | 253 + .../src/app/pages/spn/node-icon/index.ts | 1 + .../app/pages/spn/node-icon/node-icon.html | 12 + .../app/pages/spn/node-icon/node-icon.scss | 38 + .../src/app/pages/spn/node-icon/node-icon.ts | 44 + .../src/app/pages/spn/pin-details/index.ts | 1 + .../pages/spn/pin-details/pin-details.html | 127 + .../app/pages/spn/pin-details/pin-details.ts | 100 + .../src/app/pages/spn/pin-list/index.ts | 0 .../src/app/pages/spn/pin-list/pin-list.html | 84 + .../src/app/pages/spn/pin-list/pin-list.ts | 87 + .../src/app/pages/spn/pin-overlay/index.ts | 1 + .../pages/spn/pin-overlay/pin-overlay.html | 117 + .../pages/spn/pin-overlay/pin-overlay.scss | 4 + .../app/pages/spn/pin-overlay/pin-overlay.ts | 190 + .../src/app/pages/spn/pin-route/index.ts | 1 + .../app/pages/spn/pin-route/pin-route.html | 53 + .../app/pages/spn/pin-route/pin-route.scss | 67 + .../src/app/pages/spn/pin-route/pin-route.ts | 46 + .../pages/spn/spn-feature-carousel/index.ts | 1 + .../spn-feature-carousel.html | 274 + .../spn-feature-carousel.scss | 62 + .../spn-feature-carousel.ts | 83 + .../angular/src/app/pages/spn/spn-page.html | 102 + .../angular/src/app/pages/spn/spn-page.scss | 143 + desktop/angular/src/app/pages/spn/spn-page.ts | 1012 + .../angular/src/app/pages/spn/spn.module.ts | 69 + desktop/angular/src/app/pages/spn/utils.ts | 4 + .../src/app/pages/support/form/index.ts | 1 + .../app/pages/support/form/support-form.html | 107 + .../app/pages/support/form/support-form.scss | 253 + .../app/pages/support/form/support-form.ts | 258 + .../angular/src/app/pages/support/index.ts | 1 + .../angular/src/app/pages/support/pages.ts | 175 + .../pages/support/progress-dialog/index.ts | 1 + .../progress-dialog/progress-dialog.html | 114 + .../progress-dialog/progress-dialog.ts | 173 + .../src/app/pages/support/support.html | 50 + .../src/app/pages/support/support.scss | 77 + .../angular/src/app/pages/support/support.ts | 97 + .../prompt-entrypoint/prompt-entrypoint.ts | 78 + .../src/app/prompt-entrypoint/prompt.html | 65 + desktop/angular/src/app/services/index.ts | 8 + .../services/notifications.service.spec.ts | 354 + .../src/app/services/notifications.service.ts | 395 + .../src/app/services/notifications.types.ts | 205 + desktop/angular/src/app/services/package.json | 3 + .../src/app/services/session-data.service.ts | 72 + .../src/app/services/status.service.spec.ts | 16 + .../src/app/services/status.service.ts | 95 + .../angular/src/app/services/status.types.ts | 132 + .../src/app/services/supporthub.service.ts | 82 + .../src/app/services/ui-state.service.ts | 57 + .../src/app/services/virtual-notification.ts | 85 + .../action-indicator.module.ts | 13 + .../action-indicator.service.ts | 284 + .../src/app/shared/action-indicator/index.ts | 2 + .../shared/action-indicator/indicator.html | 30 + .../shared/action-indicator/indicator.scss | 74 + .../app/shared/action-indicator/indicator.ts | 78 + desktop/angular/src/app/shared/animations.ts | 111 + .../app/shared/app-icon/app-icon-resolver.ts | 118 + .../src/app/shared/app-icon/app-icon.html | 9 + .../app/shared/app-icon/app-icon.module.ts | 23 + .../src/app/shared/app-icon/app-icon.scss | 28 + .../src/app/shared/app-icon/app-icon.ts | 312 + .../angular/src/app/shared/app-icon/index.ts | 2 + .../config/basic-setting/basic-setting.html | 69 + .../config/basic-setting/basic-setting.scss | 28 + .../config/basic-setting/basic-setting.ts | 333 + .../app/shared/config/basic-setting/index.ts | 1 + .../app/shared/config/config-settings.html | 111 + .../app/shared/config/config-settings.scss | 95 + .../src/app/shared/config/config-settings.ts | 606 + .../src/app/shared/config/config.module.ts | 77 + .../export-dialog.component.html | 19 + .../export-dialog/export-dialog.component.ts | 67 + .../config/filter-lists/filter-list.html | 55 + .../config/filter-lists/filter-list.scss | 101 + .../shared/config/filter-lists/filter-list.ts | 293 + .../app/shared/config/filter-lists/index.ts | 1 + .../generic-setting/generic-setting.html | 204 + .../generic-setting/generic-setting.scss | 97 + .../config/generic-setting/generic-setting.ts | 715 + .../shared/config/generic-setting/index.ts | 1 + .../app/shared/config/import-dialog/cursor.ts | 90 + .../import-dialog.component.html | 99 + .../import-dialog/import-dialog.component.ts | 201 + .../shared/config/import-dialog/selection.ts | 185 + .../angular/src/app/shared/config/index.ts | 8 + .../app/shared/config/ordererd-list/index.ts | 2 + .../app/shared/config/ordererd-list/item.html | 14 + .../app/shared/config/ordererd-list/item.scss | 56 + .../app/shared/config/ordererd-list/item.ts | 87 + .../config/ordererd-list/ordered-list.html | 23 + .../config/ordererd-list/ordered-list.scss | 77 + .../config/ordererd-list/ordered-list.ts | 111 + .../src/app/shared/config/rule-list/index.ts | 2 + .../shared/config/rule-list/list-item.html | 29 + .../shared/config/rule-list/list-item.scss | 65 + .../app/shared/config/rule-list/list-item.ts | 221 + .../shared/config/rule-list/rule-list.html | 46 + .../shared/config/rule-list/rule-list.scss | 75 + .../app/shared/config/rule-list/rule-list.ts | 226 + .../src/app/shared/config/safe.pipe.ts | 21 + .../count-indicator/count-indicator.html | 4 + .../count-indicator/count-indicator.module.ts | 15 + .../count-indicator/count-indicator.scss | 8 + .../shared/count-indicator/count-indicator.ts | 22 + .../app/shared/count-indicator/count.pipe.ts | 18 + .../src/app/shared/count-indicator/index.ts | 2 + .../app/shared/country-flag/country-flag.ts | 45 + .../app/shared/country-flag/country.module.ts | 12 + .../src/app/shared/country-flag/index.ts | 2 + .../edit-profile-dialog.html | 322 + .../edit-profile-dialog.scss | 29 + .../edit-profile-dialog.ts | 393 + .../app/shared/edit-profile-dialog/index.ts | 1 + .../app/shared/exit-screen/exit-screen.html | 19 + .../app/shared/exit-screen/exit-screen.scss | 68 + .../src/app/shared/exit-screen/exit-screen.ts | 52 + .../app/shared/exit-screen/exit.service.ts | 146 + .../src/app/shared/exit-screen/index.ts | 2 + .../shared/expertise/expertise-directive.ts | 93 + .../shared/expertise/expertise-switch.html | 16 + .../shared/expertise/expertise-switch.scss | 12 + .../app/shared/expertise/expertise-switch.ts | 38 + .../app/shared/expertise/expertise.module.ts | 24 + .../app/shared/expertise/expertise.service.ts | 63 + .../angular/src/app/shared/expertise/index.ts | 3 + .../src/app/shared/external-link.directive.ts | 53 + .../shared/feature-scout/feature-scout.html | 106 + .../shared/feature-scout/feature-scout.scss | 15 + .../app/shared/feature-scout/feature-scout.ts | 98 + .../src/app/shared/feature-scout/index.ts | 1 + .../src/app/shared/focus/focus.directive.ts | 32 + .../src/app/shared/focus/focus.module.ts | 16 + desktop/angular/src/app/shared/focus/index.ts | 2 + .../app/shared/fuzzySearch/fuse.service.ts | 105 + .../src/app/shared/fuzzySearch/index.ts | 4 + .../src/app/shared/fuzzySearch/search-pipe.ts | 19 + .../angular/src/app/shared/loading/index.ts | 1 + .../src/app/shared/loading/loading.html | 3 + .../src/app/shared/loading/loading.scss | 52 + .../angular/src/app/shared/loading/loading.ts | 14 + desktop/angular/src/app/shared/menu/index.ts | 2 + .../src/app/shared/menu/menu-group.scss | 13 + .../src/app/shared/menu/menu-item.scss | 17 + .../src/app/shared/menu/menu-trigger.html | 14 + .../src/app/shared/menu/menu-trigger.scss | 41 + desktop/angular/src/app/shared/menu/menu.html | 6 + .../src/app/shared/menu/menu.module.ts | 26 + desktop/angular/src/app/shared/menu/menu.ts | 111 + .../src/app/shared/multi-switch/index.ts | 3 + .../app/shared/multi-switch/multi-switch.html | 5 + .../multi-switch/multi-switch.module.ts | 26 + .../app/shared/multi-switch/multi-switch.scss | 46 + .../app/shared/multi-switch/multi-switch.ts | 370 + .../app/shared/multi-switch/switch-item.scss | 35 + .../app/shared/multi-switch/switch-item.ts | 80 + .../src/app/shared/netquery/.eslintrc.json | 44 + .../netquery/add-to-filter/add-to-filter.ts | 93 + .../shared/netquery/add-to-filter/index.ts | 1 + .../circular-bar-chart.component.ts | 358 + .../app/shared/netquery/combined-menu.pipe.ts | 16 + .../connection-details/conn-details.html | 322 + .../connection-details/conn-details.scss | 114 + .../connection-details/conn-details.ts | 147 + .../netquery/connection-details/index.ts | 1 + .../netquery/connection-helper.service.ts | 537 + .../netquery/connection-row/conn-row.html | 146 + .../netquery/connection-row/conn-row.scss | 43 + .../netquery/connection-row/conn-row.ts | 78 + .../shared/netquery/connection-row/index.ts | 1 + .../angular/src/app/shared/netquery/index.ts | 2 + .../app/shared/netquery/line-chart/index.ts | 0 .../shared/netquery/line-chart/line-chart.ts | 592 + .../shared/netquery/netquery.component.html | 388 + .../app/shared/netquery/netquery.component.ts | 1270 + .../app/shared/netquery/netquery.module.ts | 88 + .../shared/netquery/pipes/can-show.pipe.ts | 22 + .../netquery/pipes/can-use-rules.pipe.ts | 32 + .../netquery/pipes/country-name.pipe.ts | 59 + .../src/app/shared/netquery/pipes/index.ts | 5 + .../shared/netquery/pipes/is-blocked.pipe.ts | 12 + .../shared/netquery/pipes/location.pipe.ts | 33 + .../app/shared/netquery/scope-label/index.ts | 1 + .../netquery/scope-label/scope-label.html | 8 + .../netquery/scope-label/scope-label.ts | 34 + .../shared/netquery/search-overlay/index.ts | 1 + .../search-overlay/search-overlay.html | 2 + .../netquery/search-overlay/search-overlay.ts | 81 + .../app/shared/netquery/searchbar/index.ts | 1 + .../shared/netquery/searchbar/searchbar.html | 76 + .../shared/netquery/searchbar/searchbar.ts | 437 + .../src/app/shared/netquery/tag-bar/index.ts | 1 + .../app/shared/netquery/tag-bar/tag-bar.html | 26 + .../app/shared/netquery/tag-bar/tag-bar.ts | 136 + .../src/app/shared/netquery/textql/helper.ts | 21 + .../src/app/shared/netquery/textql/index.ts | 1 + .../src/app/shared/netquery/textql/input.ts | 41 + .../src/app/shared/netquery/textql/lexer.ts | 255 + .../src/app/shared/netquery/textql/parser.ts | 204 + .../src/app/shared/netquery/textql/token.ts | 46 + .../angular/src/app/shared/netquery/utils.ts | 63 + .../src/app/shared/network-scout/index.ts | 1 + .../shared/network-scout/network-scout.html | 182 + .../shared/network-scout/network-scout.scss | 3 + .../app/shared/network-scout/network-scout.ts | 322 + .../src/app/shared/notification-list/index.ts | 1 + .../notification-list.component.html | 24 + .../notification-list.component.scss | 186 + .../notification-list.component.ts | 138 + .../app/shared/notification/notification.html | 27 + .../app/shared/notification/notification.scss | 48 + .../app/shared/notification/notification.ts | 65 + .../src/app/shared/pipes/bytes.pipe.ts | 28 + .../app/shared/pipes/common-pipes.module.ts | 27 + .../src/app/shared/pipes/duration.pipe.ts | 103 + desktop/angular/src/app/shared/pipes/index.ts | 6 + .../src/app/shared/pipes/round.pipe.ts | 15 + .../src/app/shared/pipes/time-ago.pipe.ts | 56 + .../src/app/shared/pipes/to-profile.pipe.ts | 35 + .../src/app/shared/pipes/to-seconds.pipe.ts | 19 + .../shared/process-details-dialog/index.ts | 1 + .../process-details-dialog.html | 131 + .../process-details-dialog.scss | 32 + .../process-details-dialog.ts | 102 + .../src/app/shared/prompt-list/index.ts | 1 + .../prompt-list/prompt-list.component.html | 68 + .../prompt-list/prompt-list.component.scss | 204 + .../prompt-list/prompt-list.component.ts | 236 + .../src/app/shared/security-lock/index.ts | 1 + .../shared/security-lock/security-lock.html | 25 + .../shared/security-lock/security-lock.scss | 120 + .../app/shared/security-lock/security-lock.ts | 97 + .../app/shared/spn-account-details/index.ts | 1 + .../spn-account-details.html | 101 + .../spn-account-details.scss | 7 + .../spn-account-details.ts | 83 + .../angular/src/app/shared/spn-login/index.ts | 1 + .../src/app/shared/spn-login/spn-login.html | 70 + .../src/app/shared/spn-login/spn-login.scss | 53 + .../src/app/shared/spn-login/spn-login.ts | 70 + .../app/shared/spn-network-status/index.ts | 1 + .../spn-network-status.html | 28 + .../spn-network-status.scss | 71 + .../spn-network-status/spn-network-status.ts | 65 + .../src/app/shared/spn-status/index.ts | 1 + .../src/app/shared/spn-status/spn-status.html | 54 + .../src/app/shared/spn-status/spn-status.ts | 128 + .../src/app/shared/status-pilot/index.ts | 1 + .../app/shared/status-pilot/pilot-widget.html | 57 + .../app/shared/status-pilot/pilot-widget.scss | 208 + .../app/shared/status-pilot/pilot-widget.ts | 115 + .../src/app/shared/text-placeholder/index.ts | 1 + .../shared/text-placeholder/placeholder.scss | 32 + .../shared/text-placeholder/placeholder.ts | 61 + desktop/angular/src/app/shared/utils.ts | 76 + desktop/angular/src/assets | 1 + desktop/angular/src/electron-app.d.ts | 41 + .../src/environments/environment.prod.ts | 22 + .../angular/src/environments/environment.ts | 19 + desktop/angular/src/i18n/helptexts.yaml | 370 + desktop/angular/src/i18n/helptexts.yaml.d.ts | 24 + desktop/angular/src/index.html | 34 + desktop/angular/src/main.ts | 94 + desktop/angular/src/polyfills.ts | 57 + desktop/angular/src/styles.scss | 120 + desktop/angular/src/test.ts | 14 + desktop/angular/src/theme.less | 4 + desktop/angular/src/theme/_breadcrumbs.scss | 20 + desktop/angular/src/theme/_button.scss | 58 + desktop/angular/src/theme/_card.scss | 110 + desktop/angular/src/theme/_colors.scss | 46 + desktop/angular/src/theme/_dialog.scss | 9 + desktop/angular/src/theme/_drag-n-drop.scss | 46 + desktop/angular/src/theme/_inputs.scss | 35 + desktop/angular/src/theme/_markdown.scss | 455 + desktop/angular/src/theme/_pill.scss | 7 + desktop/angular/src/theme/_scroll.scss | 28 + desktop/angular/src/theme/_search.scss | 10 + desktop/angular/src/theme/_table.scss | 41 + desktop/angular/src/theme/_tailwind.scss | 4 + desktop/angular/src/theme/_trust-level.scss | 73 + desktop/angular/src/theme/_typography.scss | 61 + desktop/angular/src/theme/_verdict.scss | 47 + desktop/angular/src/theme/mixins/_pill.scss | 42 + desktop/angular/tailwind.config.js | 127 + desktop/angular/tsconfig.app.json | 16 + desktop/angular/tsconfig.json | 41 + desktop/angular/tsconfig.spec.json | 19 + desktop/angular/tslint.json | 153 + 922 files changed, 84071 insertions(+), 26 deletions(-) create mode 100644 assets/fonts/Roboto-300/LICENSE.txt create mode 100644 assets/fonts/Roboto-300/Roboto-300.eot create mode 100644 assets/fonts/Roboto-300/Roboto-300.svg create mode 100644 assets/fonts/Roboto-300/Roboto-300.ttf create mode 100644 assets/fonts/Roboto-300/Roboto-300.woff create mode 100644 assets/fonts/Roboto-300/Roboto-300.woff2 create mode 100644 assets/fonts/Roboto-300italic/LICENSE.txt create mode 100644 assets/fonts/Roboto-300italic/Roboto-300italic.eot create mode 100644 assets/fonts/Roboto-300italic/Roboto-300italic.svg create mode 100644 assets/fonts/Roboto-300italic/Roboto-300italic.ttf create mode 100644 assets/fonts/Roboto-300italic/Roboto-300italic.woff create mode 100644 assets/fonts/Roboto-300italic/Roboto-300italic.woff2 create mode 100644 assets/fonts/Roboto-500/LICENSE.txt create mode 100644 assets/fonts/Roboto-500/Roboto-500.eot create mode 100644 assets/fonts/Roboto-500/Roboto-500.svg create mode 100644 assets/fonts/Roboto-500/Roboto-500.ttf create mode 100644 assets/fonts/Roboto-500/Roboto-500.woff create mode 100644 assets/fonts/Roboto-500/Roboto-500.woff2 create mode 100644 assets/fonts/Roboto-500italic/LICENSE.txt create mode 100644 assets/fonts/Roboto-500italic/Roboto-500italic.eot create mode 100644 assets/fonts/Roboto-500italic/Roboto-500italic.svg create mode 100644 assets/fonts/Roboto-500italic/Roboto-500italic.ttf create mode 100644 assets/fonts/Roboto-500italic/Roboto-500italic.woff create mode 100644 assets/fonts/Roboto-500italic/Roboto-500italic.woff2 create mode 100644 assets/fonts/Roboto-700/LICENSE.txt create mode 100644 assets/fonts/Roboto-700/Roboto-700.eot create mode 100644 assets/fonts/Roboto-700/Roboto-700.svg create mode 100644 assets/fonts/Roboto-700/Roboto-700.ttf create mode 100644 assets/fonts/Roboto-700/Roboto-700.woff create mode 100644 assets/fonts/Roboto-700/Roboto-700.woff2 create mode 100644 assets/fonts/Roboto-700italic/LICENSE.txt create mode 100644 assets/fonts/Roboto-700italic/Roboto-700italic.eot create mode 100644 assets/fonts/Roboto-700italic/Roboto-700italic.svg create mode 100644 assets/fonts/Roboto-700italic/Roboto-700italic.ttf create mode 100644 assets/fonts/Roboto-700italic/Roboto-700italic.woff create mode 100644 assets/fonts/Roboto-700italic/Roboto-700italic.woff2 create mode 100644 assets/fonts/Roboto-italic/LICENSE.txt create mode 100644 assets/fonts/Roboto-italic/Roboto-italic.eot create mode 100644 assets/fonts/Roboto-italic/Roboto-italic.svg create mode 100644 assets/fonts/Roboto-italic/Roboto-italic.ttf create mode 100644 assets/fonts/Roboto-italic/Roboto-italic.woff create mode 100644 assets/fonts/Roboto-italic/Roboto-italic.woff2 create mode 100644 assets/fonts/Roboto-regular/LICENSE.txt create mode 100644 assets/fonts/Roboto-regular/Roboto-regular.eot create mode 100644 assets/fonts/Roboto-regular/Roboto-regular.svg create mode 100644 assets/fonts/Roboto-regular/Roboto-regular.ttf create mode 100644 assets/fonts/Roboto-regular/Roboto-regular.woff create mode 100644 assets/fonts/Roboto-regular/Roboto-regular.woff2 create mode 100644 assets/fonts/roboto-slimfix.css create mode 100644 assets/fonts/roboto.css create mode 100644 assets/icons/README.md create mode 100644 assets/icons/pm_dark_128.png create mode 100644 assets/icons/pm_dark_256.png create mode 100644 assets/icons/pm_dark_512.ico create mode 100644 assets/icons/pm_dark_512.png create mode 100644 assets/icons/pm_dark_blue_128.png create mode 100644 assets/icons/pm_dark_blue_256.png create mode 100644 assets/icons/pm_dark_blue_512.ico create mode 100644 assets/icons/pm_dark_blue_512.png create mode 100644 assets/icons/pm_dark_green_128.png create mode 100644 assets/icons/pm_dark_green_256.png create mode 100644 assets/icons/pm_dark_green_512.ico create mode 100644 assets/icons/pm_dark_green_512.png create mode 100644 assets/icons/pm_dark_red_128.png create mode 100644 assets/icons/pm_dark_red_256.png create mode 100644 assets/icons/pm_dark_red_512.ico create mode 100644 assets/icons/pm_dark_red_512.png create mode 100644 assets/icons/pm_dark_yellow_128.png create mode 100644 assets/icons/pm_dark_yellow_256.png create mode 100644 assets/icons/pm_dark_yellow_512.ico create mode 100644 assets/icons/pm_dark_yellow_512.png create mode 100644 assets/icons/pm_light_128.png create mode 100644 assets/icons/pm_light_256.png create mode 100644 assets/icons/pm_light_512.ico create mode 100644 assets/icons/pm_light_512.png create mode 100644 assets/icons/pm_light_blue_128.png create mode 100644 assets/icons/pm_light_blue_256.png create mode 100644 assets/icons/pm_light_blue_512.ico create mode 100644 assets/icons/pm_light_blue_512.png create mode 100644 assets/icons/pm_light_green_128.png create mode 100644 assets/icons/pm_light_green_256.png create mode 100644 assets/icons/pm_light_green_512.ico create mode 100644 assets/icons/pm_light_green_512.png create mode 100644 assets/icons/pm_light_red_128.png create mode 100644 assets/icons/pm_light_red_256.png create mode 100644 assets/icons/pm_light_red_512.ico create mode 100644 assets/icons/pm_light_red_512.png create mode 100644 assets/icons/pm_light_yellow_128.png create mode 100644 assets/icons/pm_light_yellow_256.png create mode 100644 assets/icons/pm_light_yellow_512.ico create mode 100644 assets/icons/pm_light_yellow_512.png create mode 100644 assets/img/Mobile.svg create mode 100644 assets/img/flags/AD.png create mode 100644 assets/img/flags/AE.png create mode 100644 assets/img/flags/AF.png create mode 100644 assets/img/flags/AG.png create mode 100644 assets/img/flags/AI.png create mode 100644 assets/img/flags/AL.png create mode 100644 assets/img/flags/AM.png create mode 100644 assets/img/flags/AN.png create mode 100644 assets/img/flags/AO.png create mode 100644 assets/img/flags/AQ.png create mode 100644 assets/img/flags/AR.png create mode 100644 assets/img/flags/AS.png create mode 100644 assets/img/flags/AT.png create mode 100644 assets/img/flags/AU.png create mode 100644 assets/img/flags/AW.png create mode 100644 assets/img/flags/AX.png create mode 100644 assets/img/flags/AZ.png create mode 100644 assets/img/flags/BA.png create mode 100644 assets/img/flags/BB.png create mode 100644 assets/img/flags/BD.png create mode 100644 assets/img/flags/BE.png create mode 100644 assets/img/flags/BF.png create mode 100644 assets/img/flags/BG.png create mode 100644 assets/img/flags/BH.png create mode 100644 assets/img/flags/BI.png create mode 100644 assets/img/flags/BJ.png create mode 100644 assets/img/flags/BL.png create mode 100644 assets/img/flags/BM.png create mode 100644 assets/img/flags/BN.png create mode 100644 assets/img/flags/BO.png create mode 100644 assets/img/flags/BR.png create mode 100644 assets/img/flags/BS.png create mode 100644 assets/img/flags/BT.png create mode 100644 assets/img/flags/BW.png create mode 100644 assets/img/flags/BY.png create mode 100644 assets/img/flags/BZ.png create mode 100644 assets/img/flags/CA.png create mode 100644 assets/img/flags/CC.png create mode 100644 assets/img/flags/CD.png create mode 100644 assets/img/flags/CF.png create mode 100644 assets/img/flags/CG.png create mode 100644 assets/img/flags/CH.png create mode 100644 assets/img/flags/CI.png create mode 100644 assets/img/flags/CK.png create mode 100644 assets/img/flags/CL.png create mode 100644 assets/img/flags/CM.png create mode 100644 assets/img/flags/CN.png create mode 100644 assets/img/flags/CO.png create mode 100644 assets/img/flags/CR.png create mode 100644 assets/img/flags/CT.png create mode 100644 assets/img/flags/CU.png create mode 100644 assets/img/flags/CV.png create mode 100644 assets/img/flags/CW.png create mode 100644 assets/img/flags/CX.png create mode 100644 assets/img/flags/CY.png create mode 100644 assets/img/flags/CZ.png create mode 100644 assets/img/flags/DE.png create mode 100644 assets/img/flags/DJ.png create mode 100644 assets/img/flags/DK.png create mode 100644 assets/img/flags/DM.png create mode 100644 assets/img/flags/DO.png create mode 100644 assets/img/flags/DZ.png create mode 100644 assets/img/flags/EC.png create mode 100644 assets/img/flags/EE.png create mode 100644 assets/img/flags/EG.png create mode 100644 assets/img/flags/EH.png create mode 100644 assets/img/flags/ER.png create mode 100644 assets/img/flags/ES.png create mode 100644 assets/img/flags/ET.png create mode 100644 assets/img/flags/EU.png create mode 100644 assets/img/flags/FI.png create mode 100644 assets/img/flags/FJ.png create mode 100644 assets/img/flags/FK.png create mode 100644 assets/img/flags/FM.png create mode 100644 assets/img/flags/FO.png create mode 100644 assets/img/flags/FR.png create mode 100644 assets/img/flags/GA.png create mode 100644 assets/img/flags/GB.png create mode 100644 assets/img/flags/GD.png create mode 100644 assets/img/flags/GE.png create mode 100644 assets/img/flags/GG.png create mode 100644 assets/img/flags/GH.png create mode 100644 assets/img/flags/GI.png create mode 100644 assets/img/flags/GL.png create mode 100644 assets/img/flags/GM.png create mode 100644 assets/img/flags/GN.png create mode 100644 assets/img/flags/GQ.png create mode 100644 assets/img/flags/GR.png create mode 100644 assets/img/flags/GS.png create mode 100644 assets/img/flags/GT.png create mode 100644 assets/img/flags/GU.png create mode 100644 assets/img/flags/GW.png create mode 100644 assets/img/flags/GY.png create mode 100644 assets/img/flags/HK.png create mode 100644 assets/img/flags/HN.png create mode 100644 assets/img/flags/HR.png create mode 100644 assets/img/flags/HT.png create mode 100644 assets/img/flags/HU.png create mode 100644 assets/img/flags/IC.png create mode 100644 assets/img/flags/ID.png create mode 100644 assets/img/flags/IE.png create mode 100644 assets/img/flags/IL.png create mode 100644 assets/img/flags/IM.png create mode 100644 assets/img/flags/IN.png create mode 100644 assets/img/flags/IQ.png create mode 100644 assets/img/flags/IR.png create mode 100644 assets/img/flags/IS.png create mode 100644 assets/img/flags/IT.png create mode 100644 assets/img/flags/JE.png create mode 100644 assets/img/flags/JM.png create mode 100644 assets/img/flags/JO.png create mode 100644 assets/img/flags/JP.png create mode 100644 assets/img/flags/KE.png create mode 100644 assets/img/flags/KG.png create mode 100644 assets/img/flags/KH.png create mode 100644 assets/img/flags/KI.png create mode 100644 assets/img/flags/KM.png create mode 100644 assets/img/flags/KN.png create mode 100644 assets/img/flags/KP.png create mode 100644 assets/img/flags/KR.png create mode 100644 assets/img/flags/KW.png create mode 100644 assets/img/flags/KY.png create mode 100644 assets/img/flags/KZ.png create mode 100644 assets/img/flags/LA.png create mode 100644 assets/img/flags/LB.png create mode 100644 assets/img/flags/LC.png create mode 100644 assets/img/flags/LI.png create mode 100644 assets/img/flags/LICENSE.txt create mode 100644 assets/img/flags/LK.png create mode 100644 assets/img/flags/LR.png create mode 100644 assets/img/flags/LS.png create mode 100644 assets/img/flags/LT.png create mode 100644 assets/img/flags/LU.png create mode 100644 assets/img/flags/LV.png create mode 100644 assets/img/flags/LY.png create mode 100644 assets/img/flags/MA.png create mode 100644 assets/img/flags/MC.png create mode 100644 assets/img/flags/MD.png create mode 100644 assets/img/flags/ME.png create mode 100644 assets/img/flags/MF.png create mode 100644 assets/img/flags/MG.png create mode 100644 assets/img/flags/MH.png create mode 100644 assets/img/flags/MK.png create mode 100644 assets/img/flags/ML.png create mode 100644 assets/img/flags/MM.png create mode 100644 assets/img/flags/MN.png create mode 100644 assets/img/flags/MO.png create mode 100644 assets/img/flags/MP.png create mode 100644 assets/img/flags/MQ.png create mode 100644 assets/img/flags/MR.png create mode 100644 assets/img/flags/MS.png create mode 100644 assets/img/flags/MT.png create mode 100644 assets/img/flags/MU.png create mode 100644 assets/img/flags/MV.png create mode 100644 assets/img/flags/MW.png create mode 100644 assets/img/flags/MX.png create mode 100644 assets/img/flags/MY.png create mode 100644 assets/img/flags/MZ.png create mode 100644 assets/img/flags/NA.png create mode 100644 assets/img/flags/NC.png create mode 100644 assets/img/flags/NE.png create mode 100644 assets/img/flags/NF.png create mode 100644 assets/img/flags/NG.png create mode 100644 assets/img/flags/NI.png create mode 100644 assets/img/flags/NL.png create mode 100644 assets/img/flags/NO.png create mode 100644 assets/img/flags/NP.png create mode 100644 assets/img/flags/NR.png create mode 100644 assets/img/flags/NU.png create mode 100644 assets/img/flags/NZ.png create mode 100644 assets/img/flags/OM.png create mode 100644 assets/img/flags/PA.png create mode 100644 assets/img/flags/PE.png create mode 100644 assets/img/flags/PF.png create mode 100644 assets/img/flags/PG.png create mode 100644 assets/img/flags/PH.png create mode 100644 assets/img/flags/PK.png create mode 100644 assets/img/flags/PL.png create mode 100644 assets/img/flags/PN.png create mode 100644 assets/img/flags/PR.png create mode 100644 assets/img/flags/PS.png create mode 100644 assets/img/flags/PT.png create mode 100644 assets/img/flags/PW.png create mode 100644 assets/img/flags/PY.png create mode 100644 assets/img/flags/QA.png create mode 100644 assets/img/flags/RE.png create mode 100644 assets/img/flags/RO.png create mode 100644 assets/img/flags/RS.png create mode 100644 assets/img/flags/RU.png create mode 100644 assets/img/flags/RW.png create mode 100644 assets/img/flags/SA.png create mode 100644 assets/img/flags/SB.png create mode 100644 assets/img/flags/SC.png create mode 100644 assets/img/flags/SD.png create mode 100644 assets/img/flags/SE.png create mode 100644 assets/img/flags/SG.png create mode 100644 assets/img/flags/SH.png create mode 100644 assets/img/flags/SI.png create mode 100644 assets/img/flags/SK.png create mode 100644 assets/img/flags/SL.png create mode 100644 assets/img/flags/SM.png create mode 100644 assets/img/flags/SN.png create mode 100644 assets/img/flags/SO.png create mode 100644 assets/img/flags/SR.png create mode 100644 assets/img/flags/SS.png create mode 100644 assets/img/flags/ST.png create mode 100644 assets/img/flags/SV.png create mode 100644 assets/img/flags/SX.png create mode 100644 assets/img/flags/SY.png create mode 100644 assets/img/flags/SZ.png create mode 100644 assets/img/flags/TC.png create mode 100644 assets/img/flags/TD.png create mode 100644 assets/img/flags/TF.png create mode 100644 assets/img/flags/TG.png create mode 100644 assets/img/flags/TH.png create mode 100644 assets/img/flags/TJ.png create mode 100644 assets/img/flags/TK.png create mode 100644 assets/img/flags/TL.png create mode 100644 assets/img/flags/TM.png create mode 100644 assets/img/flags/TN.png create mode 100644 assets/img/flags/TO.png create mode 100644 assets/img/flags/TR.png create mode 100644 assets/img/flags/TT.png create mode 100644 assets/img/flags/TV.png create mode 100644 assets/img/flags/TW.png create mode 100644 assets/img/flags/TZ.png create mode 100644 assets/img/flags/UA.png create mode 100644 assets/img/flags/UG.png create mode 100644 assets/img/flags/US.png create mode 100644 assets/img/flags/UY.png create mode 100644 assets/img/flags/UZ.png create mode 100644 assets/img/flags/VA.png create mode 100644 assets/img/flags/VC.png create mode 100644 assets/img/flags/VE.png create mode 100644 assets/img/flags/VG.png create mode 100644 assets/img/flags/VI.png create mode 100644 assets/img/flags/VN.png create mode 100644 assets/img/flags/VU.png create mode 100644 assets/img/flags/WF.png create mode 100644 assets/img/flags/WS.png create mode 100644 assets/img/flags/YE.png create mode 100644 assets/img/flags/YT.png create mode 100644 assets/img/flags/ZA.png create mode 100644 assets/img/flags/ZM.png create mode 100644 assets/img/flags/ZW.png create mode 100644 assets/img/flags/_abkhazia.png create mode 100644 assets/img/flags/_basque-country.png create mode 100644 assets/img/flags/_british-antarctic-territory.png create mode 100644 assets/img/flags/_commonwealth.png create mode 100644 assets/img/flags/_england.png create mode 100644 assets/img/flags/_gosquared.png create mode 100644 assets/img/flags/_kosovo.png create mode 100644 assets/img/flags/_mars.png create mode 100644 assets/img/flags/_nagorno-karabakh.png create mode 100644 assets/img/flags/_nato.png create mode 100644 assets/img/flags/_northern-cyprus.png create mode 100644 assets/img/flags/_olympics.png create mode 100644 assets/img/flags/_red-cross.png create mode 100644 assets/img/flags/_scotland.png create mode 100644 assets/img/flags/_somaliland.png create mode 100644 assets/img/flags/_south-ossetia.png create mode 100644 assets/img/flags/_united-nations.png create mode 100644 assets/img/flags/_unknown.png create mode 100644 assets/img/flags/_wales.png create mode 100755 assets/img/linux.svg create mode 100755 assets/img/mac.svg create mode 100644 assets/img/plants1-br.png create mode 100644 assets/img/plants1.png create mode 100644 assets/img/spn-feature-carousel/access-regional-content-easily.png create mode 100644 assets/img/spn-feature-carousel/built-from-the-ground-up.png create mode 100644 assets/img/spn-feature-carousel/bye-bye-vpns.png create mode 100644 assets/img/spn-feature-carousel/easily-control-your-privacy.png create mode 100644 assets/img/spn-feature-carousel/multiple-identities-for-each-app.png create mode 100644 assets/img/spn-login.png create mode 100755 assets/img/windows.svg create mode 100644 assets/world-50m.json create mode 100644 desktop/angular/README.md create mode 100644 desktop/angular/angular.json create mode 120000 desktop/angular/assets create mode 100644 desktop/angular/browser-extension-dev.config.ts create mode 100644 desktop/angular/browser-extension.config.ts create mode 100755 desktop/angular/docker.sh create mode 100644 desktop/angular/e2e/protractor.conf.js create mode 100644 desktop/angular/e2e/src/app.e2e-spec.ts create mode 100644 desktop/angular/e2e/src/app.po.ts create mode 100644 desktop/angular/e2e/tsconfig.json create mode 100644 desktop/angular/karma.conf.js create mode 100644 desktop/angular/package-lock.json create mode 100644 desktop/angular/package.json create mode 100644 desktop/angular/projects/portmaster-chrome-extension/karma.conf.js create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/app-routing.module.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/app.component.html create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/app.component.scss create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/app.component.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/app.module.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/domain-list/domain-list.component.html create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/domain-list/domain-list.component.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/domain-list/index.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/header/header.component.html create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/header/header.component.scss create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/header/header.component.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/header/index.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/interceptor.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/request-interceptor.service.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/welcome/index.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/welcome/intro.component.html create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/welcome/intro.component.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/app/welcome/welcome.module.ts rename {assets => desktop/angular/projects/portmaster-chrome-extension/src/assets}/.gitkeep (100%) create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/assets/icon_128.png create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/background.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/background/commands.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/background/tab-tracker.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/background/tab-utils.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/environments/environment.prod.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/environments/environment.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/favicon.ico create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/index.html create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/main.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/manifest.json create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/polyfills.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/styles.scss create mode 100644 desktop/angular/projects/portmaster-chrome-extension/src/test.ts create mode 100644 desktop/angular/projects/portmaster-chrome-extension/tsconfig.app.json create mode 100644 desktop/angular/projects/portmaster-chrome-extension/tsconfig.spec.json create mode 100644 desktop/angular/projects/safing/portmaster-api/README.md create mode 100644 desktop/angular/projects/safing/portmaster-api/karma.conf.js create mode 100644 desktop/angular/projects/safing/portmaster-api/ng-package.json create mode 100644 desktop/angular/projects/safing/portmaster-api/package-lock.json create mode 100644 desktop/angular/projects/safing/portmaster-api/package.json create mode 100644 desktop/angular/projects/safing/portmaster-api/src/lib/app-profile.service.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/lib/app-profile.types.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/lib/config.service.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/lib/config.types.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/lib/core.types.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/lib/debug-api.service.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/lib/features.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/lib/meta-api.service.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/lib/module.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/lib/netquery.service.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/lib/network.types.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/lib/portapi.service.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/lib/portapi.types.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/lib/spn.service.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/lib/spn.types.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/lib/utils.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/lib/websocket.service.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/public-api.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/src/test.ts create mode 100644 desktop/angular/projects/safing/portmaster-api/tsconfig.lib.json create mode 100644 desktop/angular/projects/safing/portmaster-api/tsconfig.lib.prod.json create mode 100644 desktop/angular/projects/safing/portmaster-api/tsconfig.spec.json create mode 100644 desktop/angular/projects/safing/ui/.eslintrc.json create mode 100644 desktop/angular/projects/safing/ui/README.md create mode 100644 desktop/angular/projects/safing/ui/karma.conf.js create mode 100644 desktop/angular/projects/safing/ui/ng-package.json create mode 100644 desktop/angular/projects/safing/ui/package.json create mode 100644 desktop/angular/projects/safing/ui/src/lib/accordion/accordion-group.html create mode 100644 desktop/angular/projects/safing/ui/src/lib/accordion/accordion-group.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/accordion/accordion.html create mode 100644 desktop/angular/projects/safing/ui/src/lib/accordion/accordion.module.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/accordion/accordion.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/accordion/index.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/animations/index.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/dialog/_confirm.dialog.scss create mode 100644 desktop/angular/projects/safing/ui/src/lib/dialog/_dialog.scss create mode 100644 desktop/angular/projects/safing/ui/src/lib/dialog/confirm.dialog.html create mode 100644 desktop/angular/projects/safing/ui/src/lib/dialog/confirm.dialog.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/dialog/dialog.animations.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/dialog/dialog.container.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/dialog/dialog.module.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/dialog/dialog.ref.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/dialog/dialog.service.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/dialog/index.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/dropdown/dropdown.html create mode 100644 desktop/angular/projects/safing/ui/src/lib/dropdown/dropdown.module.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/dropdown/dropdown.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/dropdown/index.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/overlay-stepper/index.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/overlay-stepper/overlay-stepper-container.html create mode 100644 desktop/angular/projects/safing/ui/src/lib/overlay-stepper/overlay-stepper-container.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/overlay-stepper/overlay-stepper.module.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/overlay-stepper/overlay-stepper.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/overlay-stepper/refs.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/overlay-stepper/step-outlet.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/overlay-stepper/step.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/pagination/_pagination.scss create mode 100644 desktop/angular/projects/safing/ui/src/lib/pagination/dynamic-items-paginator.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/pagination/index.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/pagination/pagination.html create mode 100644 desktop/angular/projects/safing/ui/src/lib/pagination/pagination.module.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/pagination/pagination.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/pagination/snapshot-paginator.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/select/_select.scss create mode 100644 desktop/angular/projects/safing/ui/src/lib/select/index.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/select/item.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/select/select.html create mode 100644 desktop/angular/projects/safing/ui/src/lib/select/select.module.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/select/select.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tabs/_tab-group.scss create mode 100644 desktop/angular/projects/safing/ui/src/lib/tabs/index.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tabs/tab-group.html create mode 100644 desktop/angular/projects/safing/ui/src/lib/tabs/tab-group.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tabs/tab.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tabs/tabs.module.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tipup/_tipup.scss create mode 100644 desktop/angular/projects/safing/ui/src/lib/tipup/anchor.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tipup/clone-node.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tipup/css-utils.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tipup/index.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tipup/safe.pipe.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tipup/tipup-component.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tipup/tipup.html create mode 100644 desktop/angular/projects/safing/ui/src/lib/tipup/tipup.module.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tipup/tipup.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tipup/translations.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tipup/utils.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/toggle-switch/_toggle-switch.scss create mode 100644 desktop/angular/projects/safing/ui/src/lib/toggle-switch/index.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/toggle-switch/toggle-switch.html create mode 100644 desktop/angular/projects/safing/ui/src/lib/toggle-switch/toggle-switch.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/toggle-switch/toggle.module.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tooltip/_tooltip-component.scss create mode 100644 desktop/angular/projects/safing/ui/src/lib/tooltip/index.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tooltip/tooltip-component.html create mode 100644 desktop/angular/projects/safing/ui/src/lib/tooltip/tooltip-component.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tooltip/tooltip.module.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/tooltip/tooltip.ts create mode 100644 desktop/angular/projects/safing/ui/src/lib/ui.module.ts create mode 100644 desktop/angular/projects/safing/ui/src/public-api.ts create mode 100644 desktop/angular/projects/safing/ui/src/test.ts create mode 100644 desktop/angular/projects/safing/ui/theming.scss create mode 100644 desktop/angular/projects/safing/ui/tsconfig.lib.json create mode 100644 desktop/angular/projects/safing/ui/tsconfig.lib.prod.json create mode 100644 desktop/angular/projects/safing/ui/tsconfig.spec.json create mode 100644 desktop/angular/projects/tauri-builtin/src/app/app.component.html create mode 100644 desktop/angular/projects/tauri-builtin/src/app/app.component.ts create mode 100644 desktop/angular/projects/tauri-builtin/src/app/app.config.ts create mode 120000 desktop/angular/projects/tauri-builtin/src/assets create mode 100644 desktop/angular/projects/tauri-builtin/src/favicon.ico create mode 100644 desktop/angular/projects/tauri-builtin/src/index.html create mode 100644 desktop/angular/projects/tauri-builtin/src/main.ts create mode 100644 desktop/angular/projects/tauri-builtin/src/styles.scss create mode 100644 desktop/angular/projects/tauri-builtin/tsconfig.app.json create mode 100644 desktop/angular/proxy.json create mode 100644 desktop/angular/src/app/app-routing.module.ts create mode 100644 desktop/angular/src/app/app.component.html create mode 100644 desktop/angular/src/app/app.component.scss create mode 100644 desktop/angular/src/app/app.component.spec.ts create mode 100644 desktop/angular/src/app/app.component.ts create mode 100644 desktop/angular/src/app/app.module.ts create mode 100644 desktop/angular/src/app/integration/browser.ts create mode 100644 desktop/angular/src/app/integration/electron.ts create mode 100644 desktop/angular/src/app/integration/factory.ts create mode 100644 desktop/angular/src/app/integration/index.ts create mode 100644 desktop/angular/src/app/integration/integration.ts create mode 100644 desktop/angular/src/app/integration/taur-app.ts create mode 100644 desktop/angular/src/app/intro/index.ts create mode 100644 desktop/angular/src/app/intro/intro.module.ts create mode 100644 desktop/angular/src/app/intro/step-1-welcome/index.ts create mode 100644 desktop/angular/src/app/intro/step-1-welcome/step-1-welcome.html create mode 100644 desktop/angular/src/app/intro/step-1-welcome/step-1-welcome.ts create mode 100644 desktop/angular/src/app/intro/step-2-trackers/index.ts create mode 100644 desktop/angular/src/app/intro/step-2-trackers/step-2-trackers.html create mode 100644 desktop/angular/src/app/intro/step-2-trackers/step-2-trackers.ts create mode 100644 desktop/angular/src/app/intro/step-3-dns/index.ts create mode 100644 desktop/angular/src/app/intro/step-3-dns/step-3-dns.html create mode 100644 desktop/angular/src/app/intro/step-3-dns/step-3-dns.ts create mode 100644 desktop/angular/src/app/intro/step-4-tipups/index.ts create mode 100644 desktop/angular/src/app/intro/step-4-tipups/step-4-tipups.html create mode 100644 desktop/angular/src/app/intro/step-4-tipups/step-4-tipups.ts create mode 100644 desktop/angular/src/app/intro/step.scss create mode 100644 desktop/angular/src/app/layout/navigation/navigation.html create mode 100644 desktop/angular/src/app/layout/navigation/navigation.scss create mode 100644 desktop/angular/src/app/layout/navigation/navigation.ts create mode 100644 desktop/angular/src/app/layout/side-dash/side-dash.html create mode 100644 desktop/angular/src/app/layout/side-dash/side-dash.scss create mode 100644 desktop/angular/src/app/layout/side-dash/side-dash.ts create mode 100644 desktop/angular/src/app/package-lock.json create mode 100644 desktop/angular/src/app/package.json create mode 100644 desktop/angular/src/app/pages/app-view/app-insights/app-insights.component.html create mode 100644 desktop/angular/src/app/pages/app-view/app-insights/app-insights.component.ts create mode 100644 desktop/angular/src/app/pages/app-view/app-view.html create mode 100644 desktop/angular/src/app/pages/app-view/app-view.scss create mode 100644 desktop/angular/src/app/pages/app-view/app-view.ts create mode 100644 desktop/angular/src/app/pages/app-view/index.ts create mode 100644 desktop/angular/src/app/pages/app-view/merge-profile-dialog/merge-profile-dialog.component.html create mode 100644 desktop/angular/src/app/pages/app-view/merge-profile-dialog/merge-profile-dialog.component.ts create mode 100644 desktop/angular/src/app/pages/app-view/overview.html create mode 100644 desktop/angular/src/app/pages/app-view/overview.scss create mode 100644 desktop/angular/src/app/pages/app-view/overview.ts create mode 100644 desktop/angular/src/app/pages/app-view/qs-history/qs-history.component.html rename desktop/angular/{.gitkeep => src/app/pages/app-view/qs-history/qs-history.component.scss} (100%) create mode 100644 desktop/angular/src/app/pages/app-view/qs-history/qs-history.component.ts create mode 100644 desktop/angular/src/app/pages/app-view/qs-internet/index.ts create mode 100644 desktop/angular/src/app/pages/app-view/qs-internet/qs-internet.html create mode 100644 desktop/angular/src/app/pages/app-view/qs-internet/qs-internet.ts create mode 100644 desktop/angular/src/app/pages/app-view/qs-select-exit/index.ts create mode 100644 desktop/angular/src/app/pages/app-view/qs-select-exit/qs-select-exit.html create mode 100644 desktop/angular/src/app/pages/app-view/qs-select-exit/qs-select-exit.scss create mode 100644 desktop/angular/src/app/pages/app-view/qs-select-exit/qs-select-exit.ts create mode 100644 desktop/angular/src/app/pages/app-view/qs-use-spn/index.ts create mode 100644 desktop/angular/src/app/pages/app-view/qs-use-spn/qs-use-spn.html create mode 100644 desktop/angular/src/app/pages/app-view/qs-use-spn/qs-use-spn.ts create mode 100644 desktop/angular/src/app/pages/dashboard/dashboard-widget/dashboard-widget.component.html create mode 100644 desktop/angular/src/app/pages/dashboard/dashboard-widget/dashboard-widget.component.ts create mode 100644 desktop/angular/src/app/pages/dashboard/dashboard.component.html create mode 100644 desktop/angular/src/app/pages/dashboard/dashboard.component.scss create mode 100644 desktop/angular/src/app/pages/dashboard/dashboard.component.ts create mode 100644 desktop/angular/src/app/pages/dashboard/feature-card/feature-card.component.html create mode 100644 desktop/angular/src/app/pages/dashboard/feature-card/feature-card.component.scss create mode 100644 desktop/angular/src/app/pages/dashboard/feature-card/feature-card.component.ts create mode 100644 desktop/angular/src/app/pages/monitor/index.ts create mode 100644 desktop/angular/src/app/pages/monitor/monitor.html create mode 100644 desktop/angular/src/app/pages/monitor/monitor.scss create mode 100644 desktop/angular/src/app/pages/monitor/monitor.ts create mode 100644 desktop/angular/src/app/pages/page.scss create mode 100644 desktop/angular/src/app/pages/settings/settings.html create mode 100644 desktop/angular/src/app/pages/settings/settings.scss create mode 100644 desktop/angular/src/app/pages/settings/settings.ts create mode 100644 desktop/angular/src/app/pages/spn/country-details/country-details.html create mode 100644 desktop/angular/src/app/pages/spn/country-details/country-details.ts create mode 100644 desktop/angular/src/app/pages/spn/country-details/index.ts create mode 100644 desktop/angular/src/app/pages/spn/country-overlay/country-overlay.html create mode 100644 desktop/angular/src/app/pages/spn/country-overlay/country-overlay.scss create mode 100644 desktop/angular/src/app/pages/spn/country-overlay/country-overlay.ts create mode 100644 desktop/angular/src/app/pages/spn/country-overlay/index.ts create mode 100644 desktop/angular/src/app/pages/spn/index.ts create mode 100644 desktop/angular/src/app/pages/spn/map-legend/index.ts create mode 100644 desktop/angular/src/app/pages/spn/map-legend/map-legend.html create mode 100644 desktop/angular/src/app/pages/spn/map-legend/map-legend.ts create mode 100644 desktop/angular/src/app/pages/spn/map-renderer/index.ts create mode 100644 desktop/angular/src/app/pages/spn/map-renderer/map-renderer.ts create mode 100644 desktop/angular/src/app/pages/spn/map-renderer/map-style.scss create mode 100644 desktop/angular/src/app/pages/spn/map.service.ts create mode 100644 desktop/angular/src/app/pages/spn/node-icon/index.ts create mode 100644 desktop/angular/src/app/pages/spn/node-icon/node-icon.html create mode 100644 desktop/angular/src/app/pages/spn/node-icon/node-icon.scss create mode 100644 desktop/angular/src/app/pages/spn/node-icon/node-icon.ts create mode 100644 desktop/angular/src/app/pages/spn/pin-details/index.ts create mode 100644 desktop/angular/src/app/pages/spn/pin-details/pin-details.html create mode 100644 desktop/angular/src/app/pages/spn/pin-details/pin-details.ts create mode 100644 desktop/angular/src/app/pages/spn/pin-list/index.ts create mode 100644 desktop/angular/src/app/pages/spn/pin-list/pin-list.html create mode 100644 desktop/angular/src/app/pages/spn/pin-list/pin-list.ts create mode 100644 desktop/angular/src/app/pages/spn/pin-overlay/index.ts create mode 100644 desktop/angular/src/app/pages/spn/pin-overlay/pin-overlay.html create mode 100644 desktop/angular/src/app/pages/spn/pin-overlay/pin-overlay.scss create mode 100644 desktop/angular/src/app/pages/spn/pin-overlay/pin-overlay.ts create mode 100644 desktop/angular/src/app/pages/spn/pin-route/index.ts create mode 100644 desktop/angular/src/app/pages/spn/pin-route/pin-route.html create mode 100644 desktop/angular/src/app/pages/spn/pin-route/pin-route.scss create mode 100644 desktop/angular/src/app/pages/spn/pin-route/pin-route.ts create mode 100644 desktop/angular/src/app/pages/spn/spn-feature-carousel/index.ts create mode 100644 desktop/angular/src/app/pages/spn/spn-feature-carousel/spn-feature-carousel.html create mode 100644 desktop/angular/src/app/pages/spn/spn-feature-carousel/spn-feature-carousel.scss create mode 100644 desktop/angular/src/app/pages/spn/spn-feature-carousel/spn-feature-carousel.ts create mode 100644 desktop/angular/src/app/pages/spn/spn-page.html create mode 100644 desktop/angular/src/app/pages/spn/spn-page.scss create mode 100644 desktop/angular/src/app/pages/spn/spn-page.ts create mode 100644 desktop/angular/src/app/pages/spn/spn.module.ts create mode 100644 desktop/angular/src/app/pages/spn/utils.ts create mode 100644 desktop/angular/src/app/pages/support/form/index.ts create mode 100644 desktop/angular/src/app/pages/support/form/support-form.html create mode 100644 desktop/angular/src/app/pages/support/form/support-form.scss create mode 100644 desktop/angular/src/app/pages/support/form/support-form.ts create mode 100644 desktop/angular/src/app/pages/support/index.ts create mode 100644 desktop/angular/src/app/pages/support/pages.ts create mode 100644 desktop/angular/src/app/pages/support/progress-dialog/index.ts create mode 100644 desktop/angular/src/app/pages/support/progress-dialog/progress-dialog.html create mode 100644 desktop/angular/src/app/pages/support/progress-dialog/progress-dialog.ts create mode 100644 desktop/angular/src/app/pages/support/support.html create mode 100644 desktop/angular/src/app/pages/support/support.scss create mode 100644 desktop/angular/src/app/pages/support/support.ts create mode 100644 desktop/angular/src/app/prompt-entrypoint/prompt-entrypoint.ts create mode 100644 desktop/angular/src/app/prompt-entrypoint/prompt.html create mode 100644 desktop/angular/src/app/services/index.ts create mode 100644 desktop/angular/src/app/services/notifications.service.spec.ts create mode 100644 desktop/angular/src/app/services/notifications.service.ts create mode 100644 desktop/angular/src/app/services/notifications.types.ts create mode 100644 desktop/angular/src/app/services/package.json create mode 100644 desktop/angular/src/app/services/session-data.service.ts create mode 100644 desktop/angular/src/app/services/status.service.spec.ts create mode 100644 desktop/angular/src/app/services/status.service.ts create mode 100644 desktop/angular/src/app/services/status.types.ts create mode 100644 desktop/angular/src/app/services/supporthub.service.ts create mode 100644 desktop/angular/src/app/services/ui-state.service.ts create mode 100644 desktop/angular/src/app/services/virtual-notification.ts create mode 100644 desktop/angular/src/app/shared/action-indicator/action-indicator.module.ts create mode 100644 desktop/angular/src/app/shared/action-indicator/action-indicator.service.ts create mode 100644 desktop/angular/src/app/shared/action-indicator/index.ts create mode 100644 desktop/angular/src/app/shared/action-indicator/indicator.html create mode 100644 desktop/angular/src/app/shared/action-indicator/indicator.scss create mode 100644 desktop/angular/src/app/shared/action-indicator/indicator.ts create mode 100644 desktop/angular/src/app/shared/animations.ts create mode 100644 desktop/angular/src/app/shared/app-icon/app-icon-resolver.ts create mode 100644 desktop/angular/src/app/shared/app-icon/app-icon.html create mode 100644 desktop/angular/src/app/shared/app-icon/app-icon.module.ts create mode 100644 desktop/angular/src/app/shared/app-icon/app-icon.scss create mode 100644 desktop/angular/src/app/shared/app-icon/app-icon.ts create mode 100644 desktop/angular/src/app/shared/app-icon/index.ts create mode 100644 desktop/angular/src/app/shared/config/basic-setting/basic-setting.html create mode 100644 desktop/angular/src/app/shared/config/basic-setting/basic-setting.scss create mode 100644 desktop/angular/src/app/shared/config/basic-setting/basic-setting.ts create mode 100644 desktop/angular/src/app/shared/config/basic-setting/index.ts create mode 100644 desktop/angular/src/app/shared/config/config-settings.html create mode 100644 desktop/angular/src/app/shared/config/config-settings.scss create mode 100644 desktop/angular/src/app/shared/config/config-settings.ts create mode 100644 desktop/angular/src/app/shared/config/config.module.ts create mode 100644 desktop/angular/src/app/shared/config/export-dialog/export-dialog.component.html create mode 100644 desktop/angular/src/app/shared/config/export-dialog/export-dialog.component.ts create mode 100644 desktop/angular/src/app/shared/config/filter-lists/filter-list.html create mode 100644 desktop/angular/src/app/shared/config/filter-lists/filter-list.scss create mode 100644 desktop/angular/src/app/shared/config/filter-lists/filter-list.ts create mode 100644 desktop/angular/src/app/shared/config/filter-lists/index.ts create mode 100644 desktop/angular/src/app/shared/config/generic-setting/generic-setting.html create mode 100644 desktop/angular/src/app/shared/config/generic-setting/generic-setting.scss create mode 100644 desktop/angular/src/app/shared/config/generic-setting/generic-setting.ts create mode 100644 desktop/angular/src/app/shared/config/generic-setting/index.ts create mode 100644 desktop/angular/src/app/shared/config/import-dialog/cursor.ts create mode 100644 desktop/angular/src/app/shared/config/import-dialog/import-dialog.component.html create mode 100644 desktop/angular/src/app/shared/config/import-dialog/import-dialog.component.ts create mode 100644 desktop/angular/src/app/shared/config/import-dialog/selection.ts create mode 100644 desktop/angular/src/app/shared/config/index.ts create mode 100644 desktop/angular/src/app/shared/config/ordererd-list/index.ts create mode 100644 desktop/angular/src/app/shared/config/ordererd-list/item.html create mode 100644 desktop/angular/src/app/shared/config/ordererd-list/item.scss create mode 100644 desktop/angular/src/app/shared/config/ordererd-list/item.ts create mode 100644 desktop/angular/src/app/shared/config/ordererd-list/ordered-list.html create mode 100644 desktop/angular/src/app/shared/config/ordererd-list/ordered-list.scss create mode 100644 desktop/angular/src/app/shared/config/ordererd-list/ordered-list.ts create mode 100644 desktop/angular/src/app/shared/config/rule-list/index.ts create mode 100644 desktop/angular/src/app/shared/config/rule-list/list-item.html create mode 100644 desktop/angular/src/app/shared/config/rule-list/list-item.scss create mode 100644 desktop/angular/src/app/shared/config/rule-list/list-item.ts create mode 100644 desktop/angular/src/app/shared/config/rule-list/rule-list.html create mode 100644 desktop/angular/src/app/shared/config/rule-list/rule-list.scss create mode 100644 desktop/angular/src/app/shared/config/rule-list/rule-list.ts create mode 100644 desktop/angular/src/app/shared/config/safe.pipe.ts create mode 100644 desktop/angular/src/app/shared/count-indicator/count-indicator.html create mode 100644 desktop/angular/src/app/shared/count-indicator/count-indicator.module.ts create mode 100644 desktop/angular/src/app/shared/count-indicator/count-indicator.scss create mode 100644 desktop/angular/src/app/shared/count-indicator/count-indicator.ts create mode 100644 desktop/angular/src/app/shared/count-indicator/count.pipe.ts create mode 100644 desktop/angular/src/app/shared/count-indicator/index.ts create mode 100644 desktop/angular/src/app/shared/country-flag/country-flag.ts create mode 100644 desktop/angular/src/app/shared/country-flag/country.module.ts create mode 100644 desktop/angular/src/app/shared/country-flag/index.ts create mode 100644 desktop/angular/src/app/shared/edit-profile-dialog/edit-profile-dialog.html create mode 100644 desktop/angular/src/app/shared/edit-profile-dialog/edit-profile-dialog.scss create mode 100644 desktop/angular/src/app/shared/edit-profile-dialog/edit-profile-dialog.ts create mode 100644 desktop/angular/src/app/shared/edit-profile-dialog/index.ts create mode 100644 desktop/angular/src/app/shared/exit-screen/exit-screen.html create mode 100644 desktop/angular/src/app/shared/exit-screen/exit-screen.scss create mode 100644 desktop/angular/src/app/shared/exit-screen/exit-screen.ts create mode 100644 desktop/angular/src/app/shared/exit-screen/exit.service.ts create mode 100644 desktop/angular/src/app/shared/exit-screen/index.ts create mode 100644 desktop/angular/src/app/shared/expertise/expertise-directive.ts create mode 100644 desktop/angular/src/app/shared/expertise/expertise-switch.html create mode 100644 desktop/angular/src/app/shared/expertise/expertise-switch.scss create mode 100644 desktop/angular/src/app/shared/expertise/expertise-switch.ts create mode 100644 desktop/angular/src/app/shared/expertise/expertise.module.ts create mode 100644 desktop/angular/src/app/shared/expertise/expertise.service.ts create mode 100644 desktop/angular/src/app/shared/expertise/index.ts create mode 100644 desktop/angular/src/app/shared/external-link.directive.ts create mode 100644 desktop/angular/src/app/shared/feature-scout/feature-scout.html create mode 100644 desktop/angular/src/app/shared/feature-scout/feature-scout.scss create mode 100644 desktop/angular/src/app/shared/feature-scout/feature-scout.ts create mode 100644 desktop/angular/src/app/shared/feature-scout/index.ts create mode 100644 desktop/angular/src/app/shared/focus/focus.directive.ts create mode 100644 desktop/angular/src/app/shared/focus/focus.module.ts create mode 100644 desktop/angular/src/app/shared/focus/index.ts create mode 100644 desktop/angular/src/app/shared/fuzzySearch/fuse.service.ts create mode 100644 desktop/angular/src/app/shared/fuzzySearch/index.ts create mode 100644 desktop/angular/src/app/shared/fuzzySearch/search-pipe.ts create mode 100644 desktop/angular/src/app/shared/loading/index.ts create mode 100644 desktop/angular/src/app/shared/loading/loading.html create mode 100644 desktop/angular/src/app/shared/loading/loading.scss create mode 100644 desktop/angular/src/app/shared/loading/loading.ts create mode 100644 desktop/angular/src/app/shared/menu/index.ts create mode 100644 desktop/angular/src/app/shared/menu/menu-group.scss create mode 100644 desktop/angular/src/app/shared/menu/menu-item.scss create mode 100644 desktop/angular/src/app/shared/menu/menu-trigger.html create mode 100644 desktop/angular/src/app/shared/menu/menu-trigger.scss create mode 100644 desktop/angular/src/app/shared/menu/menu.html create mode 100644 desktop/angular/src/app/shared/menu/menu.module.ts create mode 100644 desktop/angular/src/app/shared/menu/menu.ts create mode 100644 desktop/angular/src/app/shared/multi-switch/index.ts create mode 100644 desktop/angular/src/app/shared/multi-switch/multi-switch.html create mode 100644 desktop/angular/src/app/shared/multi-switch/multi-switch.module.ts create mode 100644 desktop/angular/src/app/shared/multi-switch/multi-switch.scss create mode 100644 desktop/angular/src/app/shared/multi-switch/multi-switch.ts create mode 100644 desktop/angular/src/app/shared/multi-switch/switch-item.scss create mode 100644 desktop/angular/src/app/shared/multi-switch/switch-item.ts create mode 100644 desktop/angular/src/app/shared/netquery/.eslintrc.json create mode 100644 desktop/angular/src/app/shared/netquery/add-to-filter/add-to-filter.ts create mode 100644 desktop/angular/src/app/shared/netquery/add-to-filter/index.ts create mode 100644 desktop/angular/src/app/shared/netquery/circular-bar-chart/circular-bar-chart.component.ts create mode 100644 desktop/angular/src/app/shared/netquery/combined-menu.pipe.ts create mode 100644 desktop/angular/src/app/shared/netquery/connection-details/conn-details.html create mode 100644 desktop/angular/src/app/shared/netquery/connection-details/conn-details.scss create mode 100644 desktop/angular/src/app/shared/netquery/connection-details/conn-details.ts create mode 100644 desktop/angular/src/app/shared/netquery/connection-details/index.ts create mode 100644 desktop/angular/src/app/shared/netquery/connection-helper.service.ts create mode 100644 desktop/angular/src/app/shared/netquery/connection-row/conn-row.html create mode 100644 desktop/angular/src/app/shared/netquery/connection-row/conn-row.scss create mode 100644 desktop/angular/src/app/shared/netquery/connection-row/conn-row.ts create mode 100644 desktop/angular/src/app/shared/netquery/connection-row/index.ts create mode 100644 desktop/angular/src/app/shared/netquery/index.ts create mode 100644 desktop/angular/src/app/shared/netquery/line-chart/index.ts create mode 100644 desktop/angular/src/app/shared/netquery/line-chart/line-chart.ts create mode 100644 desktop/angular/src/app/shared/netquery/netquery.component.html create mode 100644 desktop/angular/src/app/shared/netquery/netquery.component.ts create mode 100644 desktop/angular/src/app/shared/netquery/netquery.module.ts create mode 100644 desktop/angular/src/app/shared/netquery/pipes/can-show.pipe.ts create mode 100644 desktop/angular/src/app/shared/netquery/pipes/can-use-rules.pipe.ts create mode 100644 desktop/angular/src/app/shared/netquery/pipes/country-name.pipe.ts create mode 100644 desktop/angular/src/app/shared/netquery/pipes/index.ts create mode 100644 desktop/angular/src/app/shared/netquery/pipes/is-blocked.pipe.ts create mode 100644 desktop/angular/src/app/shared/netquery/pipes/location.pipe.ts create mode 100644 desktop/angular/src/app/shared/netquery/scope-label/index.ts create mode 100644 desktop/angular/src/app/shared/netquery/scope-label/scope-label.html create mode 100644 desktop/angular/src/app/shared/netquery/scope-label/scope-label.ts create mode 100644 desktop/angular/src/app/shared/netquery/search-overlay/index.ts create mode 100644 desktop/angular/src/app/shared/netquery/search-overlay/search-overlay.html create mode 100644 desktop/angular/src/app/shared/netquery/search-overlay/search-overlay.ts create mode 100644 desktop/angular/src/app/shared/netquery/searchbar/index.ts create mode 100644 desktop/angular/src/app/shared/netquery/searchbar/searchbar.html create mode 100644 desktop/angular/src/app/shared/netquery/searchbar/searchbar.ts create mode 100644 desktop/angular/src/app/shared/netquery/tag-bar/index.ts create mode 100644 desktop/angular/src/app/shared/netquery/tag-bar/tag-bar.html create mode 100644 desktop/angular/src/app/shared/netquery/tag-bar/tag-bar.ts create mode 100644 desktop/angular/src/app/shared/netquery/textql/helper.ts create mode 100644 desktop/angular/src/app/shared/netquery/textql/index.ts create mode 100644 desktop/angular/src/app/shared/netquery/textql/input.ts create mode 100644 desktop/angular/src/app/shared/netquery/textql/lexer.ts create mode 100644 desktop/angular/src/app/shared/netquery/textql/parser.ts create mode 100644 desktop/angular/src/app/shared/netquery/textql/token.ts create mode 100644 desktop/angular/src/app/shared/netquery/utils.ts create mode 100644 desktop/angular/src/app/shared/network-scout/index.ts create mode 100644 desktop/angular/src/app/shared/network-scout/network-scout.html create mode 100644 desktop/angular/src/app/shared/network-scout/network-scout.scss create mode 100644 desktop/angular/src/app/shared/network-scout/network-scout.ts create mode 100644 desktop/angular/src/app/shared/notification-list/index.ts create mode 100644 desktop/angular/src/app/shared/notification-list/notification-list.component.html create mode 100644 desktop/angular/src/app/shared/notification-list/notification-list.component.scss create mode 100644 desktop/angular/src/app/shared/notification-list/notification-list.component.ts create mode 100644 desktop/angular/src/app/shared/notification/notification.html create mode 100644 desktop/angular/src/app/shared/notification/notification.scss create mode 100644 desktop/angular/src/app/shared/notification/notification.ts create mode 100644 desktop/angular/src/app/shared/pipes/bytes.pipe.ts create mode 100644 desktop/angular/src/app/shared/pipes/common-pipes.module.ts create mode 100644 desktop/angular/src/app/shared/pipes/duration.pipe.ts create mode 100644 desktop/angular/src/app/shared/pipes/index.ts create mode 100644 desktop/angular/src/app/shared/pipes/round.pipe.ts create mode 100644 desktop/angular/src/app/shared/pipes/time-ago.pipe.ts create mode 100644 desktop/angular/src/app/shared/pipes/to-profile.pipe.ts create mode 100644 desktop/angular/src/app/shared/pipes/to-seconds.pipe.ts create mode 100644 desktop/angular/src/app/shared/process-details-dialog/index.ts create mode 100644 desktop/angular/src/app/shared/process-details-dialog/process-details-dialog.html create mode 100644 desktop/angular/src/app/shared/process-details-dialog/process-details-dialog.scss create mode 100644 desktop/angular/src/app/shared/process-details-dialog/process-details-dialog.ts create mode 100644 desktop/angular/src/app/shared/prompt-list/index.ts create mode 100644 desktop/angular/src/app/shared/prompt-list/prompt-list.component.html create mode 100644 desktop/angular/src/app/shared/prompt-list/prompt-list.component.scss create mode 100644 desktop/angular/src/app/shared/prompt-list/prompt-list.component.ts create mode 100644 desktop/angular/src/app/shared/security-lock/index.ts create mode 100644 desktop/angular/src/app/shared/security-lock/security-lock.html create mode 100644 desktop/angular/src/app/shared/security-lock/security-lock.scss create mode 100644 desktop/angular/src/app/shared/security-lock/security-lock.ts create mode 100644 desktop/angular/src/app/shared/spn-account-details/index.ts create mode 100644 desktop/angular/src/app/shared/spn-account-details/spn-account-details.html create mode 100644 desktop/angular/src/app/shared/spn-account-details/spn-account-details.scss create mode 100644 desktop/angular/src/app/shared/spn-account-details/spn-account-details.ts create mode 100644 desktop/angular/src/app/shared/spn-login/index.ts create mode 100644 desktop/angular/src/app/shared/spn-login/spn-login.html create mode 100644 desktop/angular/src/app/shared/spn-login/spn-login.scss create mode 100644 desktop/angular/src/app/shared/spn-login/spn-login.ts create mode 100644 desktop/angular/src/app/shared/spn-network-status/index.ts create mode 100644 desktop/angular/src/app/shared/spn-network-status/spn-network-status.html create mode 100644 desktop/angular/src/app/shared/spn-network-status/spn-network-status.scss create mode 100644 desktop/angular/src/app/shared/spn-network-status/spn-network-status.ts create mode 100644 desktop/angular/src/app/shared/spn-status/index.ts create mode 100644 desktop/angular/src/app/shared/spn-status/spn-status.html create mode 100644 desktop/angular/src/app/shared/spn-status/spn-status.ts create mode 100644 desktop/angular/src/app/shared/status-pilot/index.ts create mode 100644 desktop/angular/src/app/shared/status-pilot/pilot-widget.html create mode 100644 desktop/angular/src/app/shared/status-pilot/pilot-widget.scss create mode 100644 desktop/angular/src/app/shared/status-pilot/pilot-widget.ts create mode 100644 desktop/angular/src/app/shared/text-placeholder/index.ts create mode 100644 desktop/angular/src/app/shared/text-placeholder/placeholder.scss create mode 100644 desktop/angular/src/app/shared/text-placeholder/placeholder.ts create mode 100644 desktop/angular/src/app/shared/utils.ts create mode 120000 desktop/angular/src/assets create mode 100644 desktop/angular/src/electron-app.d.ts create mode 100644 desktop/angular/src/environments/environment.prod.ts create mode 100644 desktop/angular/src/environments/environment.ts create mode 100644 desktop/angular/src/i18n/helptexts.yaml create mode 100644 desktop/angular/src/i18n/helptexts.yaml.d.ts create mode 100644 desktop/angular/src/index.html create mode 100644 desktop/angular/src/main.ts create mode 100644 desktop/angular/src/polyfills.ts create mode 100644 desktop/angular/src/styles.scss create mode 100644 desktop/angular/src/test.ts create mode 100644 desktop/angular/src/theme.less create mode 100644 desktop/angular/src/theme/_breadcrumbs.scss create mode 100644 desktop/angular/src/theme/_button.scss create mode 100644 desktop/angular/src/theme/_card.scss create mode 100644 desktop/angular/src/theme/_colors.scss create mode 100644 desktop/angular/src/theme/_dialog.scss create mode 100644 desktop/angular/src/theme/_drag-n-drop.scss create mode 100644 desktop/angular/src/theme/_inputs.scss create mode 100644 desktop/angular/src/theme/_markdown.scss create mode 100644 desktop/angular/src/theme/_pill.scss create mode 100644 desktop/angular/src/theme/_scroll.scss create mode 100644 desktop/angular/src/theme/_search.scss create mode 100644 desktop/angular/src/theme/_table.scss create mode 100644 desktop/angular/src/theme/_tailwind.scss create mode 100644 desktop/angular/src/theme/_trust-level.scss create mode 100644 desktop/angular/src/theme/_typography.scss create mode 100644 desktop/angular/src/theme/_verdict.scss create mode 100644 desktop/angular/src/theme/mixins/_pill.scss create mode 100644 desktop/angular/tailwind.config.js create mode 100644 desktop/angular/tsconfig.app.json create mode 100644 desktop/angular/tsconfig.json create mode 100644 desktop/angular/tsconfig.spec.json create mode 100644 desktop/angular/tslint.json diff --git a/.earthlyignore b/.earthlyignore index 9b694cb7..37c45b4d 100644 --- a/.earthlyignore +++ b/.earthlyignore @@ -7,4 +7,9 @@ node_modules/ desktop/angular/node_modules desktop/angular/dist desktop/angular/dist-lib -desktop/angular/dist-extension \ No newline at end of file +desktop/angular/dist-extension +desktop/angular/.angular + +# Assets are ignored here because the symlink wouldn't work in +# the buildkit container so we copy the assets directly in Earthfile. +desktop/angular/assets \ No newline at end of file diff --git a/Earthfile b/Earthfile index 1ca151dc..3638f0d6 100644 --- a/Earthfile +++ b/Earthfile @@ -52,7 +52,7 @@ build-go: ARG GOOS=linux ARG GOARCH=amd64 ARG GOARM - ARG CMDS=portmaster-start portmaster-core hub + ARG CMDS=portmaster-start portmaster-core hub notifier CACHE --sharing shared "$GOCACHE" CACHE --sharing shared "$GOMODCACHE" @@ -112,7 +112,7 @@ test-go-all-platforms: BUILD +test-go --GOARCH=amd64 --GOOS=windows BUILD +test-go --GOARCH=arm64 --GOOS=windows -# Builds portmaster-start and portmaster-core for all supported platforms +# Builds portmaster-start, portmaster-core, hub and notifier for all supported platforms build-go-release: # Linux platforms: BUILD +build-go --GOARCH=amd64 --GOOS=linux @@ -131,46 +131,61 @@ build-utils: BUILD +build-go --CMDS="" --GOARCH=amd64 --GOOS=linux BUILD +build-go --CMDS="" --GOARCH=amd64 --GOOS=windows -# Prepares the angular project +# Prepares the angular project by installing dependencies angular-deps: FROM node:${node_version} WORKDIR /app/ui RUN apt update && apt install zip - CACHE --sharing shared "/app/ui/node_modules" - COPY desktop/angular/package.json . COPY desktop/angular/package-lock.json . + COPY assets/ ./assets + RUN npm install - +# Copies the UI folder into the working container +# and builds the shared libraries in the specified configuration (production or development) angular-base: FROM +angular-deps + ARG configuration="production" COPY desktop/angular/ . -# Build the Portmaster UI (angular) in release mode + IF [ "${configuration}" = "production" ] + RUN npm run build-libs + ELSE + RUN npm run build-libs:dev + END + +# Build an angualr project, zip it and save artifacts locally +angular-project: + ARG --required project + ARG --required dist + ARG configuration="production" + ARG baseHref="/" + + FROM +angular-base --configuration="${configuration}" + + IF [ "${configuration}" = "production" ] + ENV NODE_ENV="production" + END + + RUN ./node_modules/.bin/ng build --configuration ${configuration} --base-href ${baseHref} "${project}" + + RUN zip -r "./${project}.zip" "${dist}" + SAVE ARTIFACT "./${project}.zip" AS LOCAL ${outputDir}/${project}.zip + SAVE ARTIFACT "./dist" AS LOCAL ${outputDir}/${project} + +# Build the angular projects (portmaster-UI and tauri-builtin) in production mode angular-release: - FROM +angular-base + BUILD +angular-project --project=portmaster --dist=./dist --configuration=production --baseHref=/ui/modules/portmaster + BUILD +angular-project --project=tauri-builtin --dist=./dist/tauri-builtin --configuration=production --baseHref="/" - CACHE --sharing shared "/app/ui/node_modules" - - RUN npm run build - RUN zip -r ./angular.zip ./dist - SAVE ARTIFACT "./angular.zip" AS LOCAL ${outputDir}/angular.zip - SAVE ARTIFACT "./dist" AS LOCAL ${outputDir}/angular - - -# Build the Portmaster UI (angular) in dev mode +# Build the angular projects (portmaster-UI and tauri-builtin) in dev mode angular-dev: - FROM +angular-base - - CACHE --sharing shared "/app/ui/node_modules" - - RUN npm run build:dev - SAVE ARTIFACT ./dist AS LOCAL ${outputDir}/angular - + BUILD +angular-project --project=portmaster --dist=./dist --configuration=development --baseHref=/ui/modules/portmaster + BUILD +angular-project --project=tauri-builtin --dist=./dist/tauri-builtin --configuration=development --baseHref="/" release: BUILD +build-go-release diff --git a/assets/fonts/Roboto-300/LICENSE.txt b/assets/fonts/Roboto-300/LICENSE.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/assets/fonts/Roboto-300/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/assets/fonts/Roboto-300/Roboto-300.eot b/assets/fonts/Roboto-300/Roboto-300.eot new file mode 100644 index 0000000000000000000000000000000000000000..826acfda9102ca6aba858813c72fa34b6968da36 GIT binary patch literal 16205 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 GcK$C}BGQQf literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-300/Roboto-300.svg b/assets/fonts/Roboto-300/Roboto-300.svg new file mode 100644 index 00000000..52b28327 --- /dev/null +++ b/assets/fonts/Roboto-300/Roboto-300.svg @@ -0,0 +1,314 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/fonts/Roboto-300/Roboto-300.ttf b/assets/fonts/Roboto-300/Roboto-300.ttf new file mode 100644 index 0000000000000000000000000000000000000000..66bc5ab8e299e9948a6fce6b7867cc638bc703c7 GIT binary patch literal 32664 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 NR0RNNh`?q5{|9c~2uc6| literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-300/Roboto-300.woff2 b/assets/fonts/Roboto-300/Roboto-300.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..4411cbc8754cf7ec78501ba711efabe15b936675 GIT binary patch literal 10324 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 Z-kh5%$qv-l3y!);OTS1+V=ll=NB|b$lez!^ literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-300italic/Roboto-300italic.svg b/assets/fonts/Roboto-300italic/Roboto-300italic.svg new file mode 100644 index 00000000..ea86b201 --- /dev/null +++ b/assets/fonts/Roboto-300italic/Roboto-300italic.svg @@ -0,0 +1,327 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/fonts/Roboto-300italic/Roboto-300italic.ttf b/assets/fonts/Roboto-300italic/Roboto-300italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ef1d13ceee0fbcd9bf33ac437372a0abae2857f5 GIT binary patch literal 34384 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-300italic/Roboto-300italic.woff b/assets/fonts/Roboto-300italic/Roboto-300italic.woff new file mode 100644 index 0000000000000000000000000000000000000000..fc4a8b5ae7d24b1fc390521113ee34c0e3a66b87 GIT binary patch literal 15004 zcmYj&1yCJL(Cr06aM$1t!JUh{ySuwv(2FFvyL)hV*Wm8%?(Pnc@BiPcdQ*FLs?VOT z?%kT2nXT$^lNS>MfC0W5R|Wv_zg{Ke%l;qcKkNT*;tKN0005W>007ww0KhM>N<1!$ zE2|0t08r3hI-M_sBv522$tyE3e{m*Xw%QjAa)hf_jBO1azPR=;{{jF20=VN(tYvQK z{Kdiir-T0=Km*XsZ9L4rxG$c@?<=PS;r`5M3lm#+0D$%GOJn+lqtDN~Z3|PwFF&^b zVzU2-C%ZC8i!btvOZc)$zCZ>akEm;5`|Hb>tr-9SSNV$Tst?PvXJc>tr2~G&1grQ$ zr>UUoiLIggS4_^Y`~XD%0Rj=C$* z026nx#3YCmGEGb|d7`?yjI@O%+q`xgx9F9E2HRCL=MHYO)y5G5-2H~ri>{H8;~VbH zP*2C*zdQ1fh%qCud=Z~3&(i9Q53<=$TWu}dwuD}zZ7J6m99~Zy=sOT8+XC|XcX#Xg zoxz`#O8IT{v)gA+staR7SCsi# zwJZLXa&NO`cd@N^vFk&yB}iZdr@cJj`cNDFj<-cL2psgFSCB(!m{XL8h~ybD2AXK2 z@wN0d7$<;>ImvMKolzH}HpYtV-#3Q+T=ykT2i~4JVrtO9?US^K&?nREQ-iV^7KCCu zBud_*tWH^d^V-F&*Jv)|A4j3~{BN2UxQXRQlyB;JNGGi>I6d_AV)kp*7KS~SiEvHh zAFPv*M#cB?Rvwv-6>P+(r|kw+?NYtEeFT5olu<5bkx#BrRSyCwyj7cuS1P4222G&# zhs$d-(|pq&e_d|RvdhuX?v_31b{cl|Q2GO_NOBeBZ@(KA3nft>m)4iYg}OE!LNoVs z4aElU8G48iBs%&9NXCEa(v+I$oBY)wOCCy8vcAQCe@8L&pLjjk)V&I*>~I1^pML+#xiNS(~wE3O%-H9@uO9*_`fevAivZpqf!}Gp@eVoqgXF>)4#phRPYfD zB=s0+N;zQPaJf8rCVsQpI?6ai+w(xPYD{_d9e`_Vcv^c7Xq>ewM=YoCKuU;tM>&%M zfXGVp?B$R6VgaecRWM8HfU~~-=OMLcA)}}X(9FyR*-KaBy6aNf-p>v#lvi>^5|N7K zl{3q9neR)GuLUm4(g3bSEs1BZ8&F@?De1WX-RfCWFX(fHP@hlOyp*ZjtP6K*Uu^y!c2NiwroA@;!1abqxJ4KjM;lI9F;&2Efx)I|ld%7Osq%H8xGF`quM0Y{E~ znZH3|k@K_RVl882w0OBa+n(D_{#VV4PZ4Nf1K~vRDPz!uA;`+2u(Kl6v#EbyWKax} zBuKUM<4J|~!r*U2D4t2e!BGkoL{cbo$FP_%LxACZ0%Vpl7d)8Dz`<;qgpg#3 zK1Gk0WWZA+^fmtWErgE*9xujjwVcS*EYpahNV9Zbyc7K2vtKH%`A#w>IcHZuX-+;i zlHW?C5@^vep|zx^`ebF8>i~B!+0cG4Awgte{qJsv?T|Kv&B9=y?rjmlDJnZ()?r}* zOnKb%-eZY0+BAm3DC#vkXg>WVpE_7{{U#Xf_NVaJJ`^YsoNa#oi66$u#PftL1`#N^ zAgB~YgdBG;@_r65a6tg%v>Rs#(cQ0K*v(FgHSAM@jlVJQw)`?KgMVncN>advsWOrO zc$kPX3fp|msK0Ev1~)zuIwEV&F2JhMDiDj~-K*O%xU+o$OZkRq1o+C?dlVwVP8|?9 zeV^C3p4EzA-9Ow{evbXM>@;#WjvLs3C`v0<(=$lCxHXrjgLU=Q91{D6#|N& zZypBZF0QkLM74h5R$o|49z2aLFJGMfa%DgJ@qh#s+}rcxFVPx0EYd2kqdUArddN|mbw^C*zq73wf6R_Z}R zyzjTwU>8KPJp{qx*IvDvb| z+s2*Fd&1;5#OB|{;tA5frlhz|K7t|39BMW>-PD5(V&?2oRJ2hWO)UM(VO|jOQgJJv%x*`GZp;F7CsU0t#9iYe6XZ41NE1 zrLzuzYrkGi(-O(|HVO(Hv{_bH<4z5ugv?hnH^KPFjV`a)!1YXC<%#Fa=S=4VVQ`#q zw`;2((5W&PB{1M=`ReyC7k0WBbO&Em@ayX}iod!M_P#| zE!Ueyy|jd+0} zp!1l$^ze02gxJ_fZ(`GIN7#59lu010Guh#P5yfa`*iN7vadfG_#ELU;X=iFu`;23o{+qRbnBhcLC>}Q;N;MGoHd&g)ry-SQ9hG zAc^jf!ff@jmiAU_)C>0b-pWA(b}^D~GUiA?!v#FnJ2tNpKUsmS{mT<}4x55%;?2jk z)=r+pzAX$aEypQv=Bd zoJ(D$3__2ChYxPKhce&wn)pmdi+QuQGCR@s*_8Kqyt5vE(!zL8U1_HohcfYo=q1g^}1GL zkqT2*!#}eduWQ^Cen|hFJ%%GP5SI`!7&VWquEUOk*TEE163E#wfPudn1j9t)-EQ^X z_@lbv@|oc+!&?)BYOK}L6U((zX%pnd4HBT|@&9Y5y~y`_BKvz$HT&6NwZbN3^J!kMUoi?J{fo-8taK#A}aC!uXalTH3UUONi#d zrOC=GN3Hv|t|h5BihF_f~Go`d~qc1(U5nqvt_g|;bergvq{>{Z}nzS^CYH+uw( zLnZ;E&K1#pR6&|#MY4Zbi_7M>?{-x6VZrmee*%!a1xXKtu+8=^0?E?4tnEONo#JJR zbOGopRvSg+OzB?fXf_}DqV~!(zZW_S&6-;n4q1TLDZ_?l05xq2U-BFq zH;aQE2##ZrgusJq`g6q7-ccd|xBE#U-|q#&iIM3Gt1hJrDCquOx7|3QtoqI+y~8ZM zx81A1rL~5*a>9(#vsqd?kp6Hxj0pCeLgj){>0E8&Ha>%MO+*azjQh8@c;=(F^kziz zehoji3=IY(5m|wTiW%~*A12ax8*3Im;Ke*gR2BQXksAR49m6j)muTECe7%baT|Q8Q zepH5l-9B7eno1AcmG-yxxyT52QR3}>9{a=KV)wXL_hd&V$JJ%$^rJvrtsBdpv7@DA zr_(ER)OL{RsDGbb$)@rClTIxmfku_6xCQvp2^n-=GTP zCt@l|qIrE^o7T@@!BZdCR69OvHVA!1SHi8LAJdm~T&7)mq$zZBjoO@LH}E|eE=u5A z(r*r)Sr0G5SOlMX2x)~@bgQh=Dou1ks?x$r=}yPBY9qYcbZc;pMLUxS?kQbOp@sId z&s_$WxRBeU=ASXEw&M3_gklXFEe?+9&cUW~qxO^(@xrkk4`Xkzl!|`XVYt5u^0FT8 zWbx#9cu;zN4t1BayZ)(u$SF!bd#&un9@>J?lhVQchz627mIobH{=Hk+m|>uj6_{tg z`@M!0MJj*Mi!4Eba=p=BUHdES5O!-zd?Sapr1+uymuwU#`(C=;=MHy?X}8>if9nLW z1&UcS<#BIe4qWQyZEis~g@ zfRu>{#hhQ$dr|2#(X%QWSnjfaDX5qKqi_Rp)Oj$n$=UF+692Qi-*9yYrS~jDfU9M< z$M%p7vVcQZM{S(8lWoA!^`IPW;!R6Iu+21~$ZE=40ZQ;uZlG|( z?GHAW-~4@SH^c1>vB`A9qGr^JVOYQP5Vzv^dM83yk_W%Vh|sh05FTq#n*tG=ojcWN zn=lMz~v&wVBF!`zZs>m7Xjv14~# zcb?+yLIga)CgLxmE zLZ~x&&nAK6g-`6zhm#Ob@m{syp+l+-{XR!voYLGhFO0vxVaC{PnIN@vM)i5@aGr@_ z%0&In0H05aGvxJGvqlMe1N|Nc(IZI~|+Rp-E$eLZde-qLW<06+I-Zc!! zF+|JNPmy1_t+#hw7F}cbb#}p44Pz zFOzYfXMnINZu0Ju#czrkn#UC#-WkSJJ_R4-nXpa`hJ(X}*K2m<`c4T1_jg07P10?? zqSJQ8SrH>cF_dI(v=)PO4PE`-m`Exj%!x^^BE4H>phhG)q{DgH25uw~yrOyY%|;K< zPaAc6fehb=k1`kRtDbW)D;D^ihS0zC2Y3c`BW zE8^cJvbmSdoLwVnMCGb_>uF(c`&S7E5&*W5np)2Wi8ITu^$zaiS6g1s8R0-Rv#M7@ zu59oPyJmlkTrVgH;;KRi^B+cd>i7G9r$QFaA4t?ZLT4#^+Yg6&$X8qcKKRdZy1 zyc_z;NcU%u%KN&{M`lN1_emA~aOi?fDjAMR5tBR21__cfU-Q4!$y(mr%Ya+hyem+b zk0;p`2FtVcXc054m7)Zi?9phJO`FC(6>!cDEbouX2eFydsuV-bCXUBK&)yN&Z_Ra$ zKLmQ`@X-|5caK6)Hhv3?f;o>gVNHsEbd9UjUC%u(z%UDi&G1SI>X84AC*H#FS<&g` zDsnEX@|&2CF8}P~Fh$Kir0gN!gq&3F{L#zM#Fk}paDI8E_J*i{-P{#cebtQM#Qo%r+3BNMxdXo86$r7PP)gWfKMej+@jFIzI^|j0OGV#BaUyj0WqKpL z04KX)(2RZ!Ktc4zvF0!Kv@OQTJxslbV65LyVMgOfhsHR#q>^zx@n>bvrX|EzyH3xT z4Ih-wYBs4(LodAXp&OO)C)g`l)YoTGVd=IK>|L~GR;F?{S!d7=mIkd<)dmK=y;>2b ziE;m~JhIfL(wd`FZRmHKo~K?oS2Npc2U32|?J&3_YZ~9-ons~bw`7@OlgIRDq32fn zf7n}M-46UFTeKD~twKcc*^`8*TAqQr)Wo`6ZsV0QL>UM}$$9i^dEnpvDEOztQQN_5 znUGjy^;8Wg3hXY~{IJu1Q9K;*@#AgorTf-6YrMnQi+z zA-gQ^E3O_Y{iM_aAcv`^1xo1Y-ZcAivHU45m#FW<-`H;Z4>j(Z@qV~&`dB$Ud(Gnx z#UBdj96xfcpC~m#1`nS;XIy6K{mLuQ*@@^1fPg!+$;;^{&}pK1ZpUauSpt8hHu0b3 zoQ1l`6Tt)18k2_@#@wBx^k3q2dlj98b7m0Z-UHgx>%E7U&F=UkApyB)Jvq%E5GW>I8q$EG$vOxAwsM@CpWma^atuZ(= zL@(Hmuk3VMt|#$#xH)7%u9;W;Ouf6cx$#cCTsX~jw?pzM9bWBwMclhNLm$_klO2uV ztzntSCg(1yOXqyWWzA8w7j{Q$c()A5_nCjEIY(F#CllDgVnLS{-d5Xz*QH93jejhh zbbCq(Jq}z}aJa=?Ia`0G=~9|qMNZQMzOL(tk}P#D98|to&1)8ajscOfFh+w7-)LR4 z{_eYzY4e8P^)opsFJqaH20js2stneJ`(Bns>Obo_LYwix`?iZ9B8^6woSN?#?Pw-a zeTJpSfNyMGLQgZWz0vp#S!P*WLHTW} z=SyA!96eD$12ZX}&5G$^|401u__z001f0O@1xuK+pq~7Ep|HAvvDM(?RE(QRVTe^O zA2G@S-r%({!=E@C>B|@KZC*2;Y)~PwJz!*fzNp3h05B~oaTPHkyYv(x3z2NQ0vf}K;#F+qi38O z!aPe@uAn6D&3w@1tsJ}?$;9Y|{NxJWnGGdwi1gGN6H0+D|FN^)Sli6^B9wP{4xYz; zoov%B=IkxYN3;9uj4M+v3gY$EKv7hRjAaP=TY8#P9!TOD&2s-WjDf*CPBw_4Ix`A8 zojBBwL%Yp_&11dSJHH3-HGcN!%u1H_+0Ty5wJ@!AGpvCZCVCIc+1ye-r3J-FzY_J`aN^mv+l?z!qd=KkxJW(+{cbt&oo7hsOfUCQBo3o8!$%B93JXz5JSj$2Bg>>|uAwEU!d1 zODK@u+e*H9q2 z(X9pPLj8ifvR)@FuR`6`4)BTb_|uTIj$d0&QE(U`M3j*g*O4icYUt#j-l^Hz!O;HT z?SWb!AO02~i$zt+RK6RIWzN#g+M;qOJd_DyahFfB++zBuZ1 zeo)gU2H~0z%&XROO4`)Jr6)Z@-WK~n;6^^w-%K}Yg41VKD2qR(htFanY&u9*%>-kb ztD~@p3%2oF2RP3^^AC5wi_*%8s8%Q&{9qXkXCo{cI}He7MT9-@PTh+6Npg;_UJZ;5 zqQ-=cgGC<4f48PXcsbc*oHL)!OxS3YbVyW^Jn;z9*|5n(qIW{@Rbc_{nk?Q{>iV!`j2XJ zBRl%DYi{Tj$jlD*xoI_LVlteExybB~Py+`Jc5TLe`i$BN z){|V1T4c+q*Q?y{_CRi@ZI5{F3oMdNRJ-IewBz%U3H+0Dv-oSuZEGZ@3}n(zHfld; zHwKc3plaxoe@vy~8_<@=YI(KSKdf}tyJCI@rn^t!bhZ}!OjXr1Hq)n8Jd3DFCU{*FjtG=+|!s=i*REI8ykS>by|JG;pa|lYQfwR4DTVPfRI;sA+!R8)dl_u ztNPaPMBZ|35}I!}7k0_|?>=esxO246821+%*RQZ>huW;^Y%6kj6Zzb|13@(CCDCKX zEPe?6Xz;Hs*Y5MiuFxut>v<6jv#*55Z3?XV{sx`qK~_>MHLOx!QEGBKEK79JxCTR9 zI^{K3i<`l!qH$;phN#YFrt+SwNXXjmjR-x`7|y3Zjq+YuY?p}*Io#KyNJ8Y`_Nxe2 z0nd$z_s@d}Ol(v@oiS>k7&1%Zv2nS9i6T?Uu&*zBT76k*NGSRFW+o{qmuu*+Gqjro z-^<)c?b~oK7rK*gm6$`juQu4Hn;SK|Nkdzw@sBZ@mpoXe#%FlslF7-l1Emp^(6OZT z?_C~yM(vXNE(?iA`xu@!v!dd`v3E-jE(j}iB0ChWV)qyz6(O1ec|`Y)>{wLE1Qno@ zDfV`-X&nNB$h2+buD5Tt0Cr*1UkbiX%2|9ETLVgJ$>0hF5WduZZ~Jz`_C+efvn+y?QSEeKvNWb2*SW1 zF+MU=-(rGbGE^8imf!4lN7~ERXrZurs3>BHcFh(+=~#ptR>4Ju-~ZUaakJx%McW~k zC!?>GKig+=iHzASb&b!icQx2H_R&{jmU;eTA0QFGe+Ef9z}*k{H_nkf^WFWtp+IK+kN#bcJnB)(p|r?wbb1Vvg75>B zeBP6(Jq+fH2V(E$Q5pDs9);&K{@mqlnYK&L*>C#QpYvR%O~HilQ@h{~M)K%U;eiAE zf5h?TT3-BS(pQ~;@7(R7yphU}MB=NwhLG<_P2C6c1Vq1`tvdCR02?hQnUQgEWb9Hz zp4%s|L1|W$jmrAsgvTvilYa$WaBZludEZ4$_d2sMylH~w%G$UX%BdHbh}wazTl=2= zc1PW3?fi|h`ht!QoAY^U7iH~k2IfZ#9_WuJEi5*x)3+R>O#wS|im!*!B)>k=IcKxR zbL+1%bHhKNVH%ppJuR9Ks^hGrqE37b24a_NOS}+CbT$1LXm=M~m(MkFX=v|E(R7HD zbgBRG?^@T|Jrx+90q?L~ErC9j6Zpc(X7pRLh0g&~CBSxLDR`ol5!Oq}s0}KM8@x_X|Er1mbbK!8^^YDv9 zeNcH_tBr2MUXpc1^YE!Kw)5;eo47guY17;7CJu9k^kG%Y!H(m_a2n@W#%FV*GV`6w zaMtBfN(U;U_w({n-cQ_Ml(#KBJtqL%ZEf&AtL3Ue&pSh~7@^DtPsG$<5!OYKyX(Wj z2l07h5*rMVor^(kt_YD(a|(wTUWGAMVFChQg)}f+YDe|2sb7oNu8?F;8ae}kcOJ7;x}}^82wI1 zKExu(yP@;Y9z~Wfj72fmDDo9jC0=m)Xz|5crd@%|xUkam$HZjW{IQEh+L(j&{Y&-fY#QWJpw#LNGFlP(25BL zAgUo9%E}?!25~P2MlTH;MP1wQ(ujYLE6BiqPvvvG5==(Jdz(L;cM%jKfkZ@A58l|` zg?J~E94CkGV_B2R3*3T=qh1a#>fVYH-Bh57np#vM=y&L}B!ZnAqXHdU&qYKz2nQ`Fl zNh-rPYLv3;;yr!JR)_3da=8Y047ji);L<6*7QT2y2$hp#F`LoT5;)~wzbrlG!aMi^ z%4EbJt%Sf@z~-)o`MP=@8+bCW>166>npdJ-NVq(Xj*OZ^?2o_2Zt zZ;w(Ag0AiTV(k#b4dZMZg;gtNrEH z+Uj-ns`@%D2u#kd=!qtDz+V37au3zsMm^P$F_-ycdq~w$doy=22Ig##<0@3cDd4=p zPnb8STj6q?sLcM|p0UJ2&G+9b(Sho_S<+-JcmJI-YI$-mXgA8Uh54I3=n&GLLI7+O z<)Lqgzkle|kl3=~L^F6GhGwtSNF5XKF$qlsc4Pnv;zQ3cR6|MvyAQ4g(H=0832E>* z5{Mxg;x6TOGG@`Kvb03nw0$ePVksSLn1v-49R5v>E;|`6r}d_TPi=q$$VI7VCas47 ze55qN_GLa6%1YBmQ%t=soYc36bgYNP$u*R#N*``}$Gw{)tm>7KUSoHBQnxKO6Qpiy z-g_?fysZ|Ti@M=79z2X>lm3PG^qL9Uy69-+Nslp<&HSV*(+G526;>Xq`>A4}U#kv9 zWV;ko8uSjyjBsYCRczGOfs*^av1k>t707AH{QNVpH4TRy&mOMAiI4Dj3;6nB<64@j zYR3jPR^~I;l5`bsb%qm@(tA8BHvrB(VUM=wp}^PW05|Wj5prm;872qo%$8pI-og?K zlvNejQI5ZdQ;CX*u}Ud#$f|S$?PlB~)(a}}PFJqSRr_>yIvsUyMiNCN~@*6suB^MM~#77SV80a8xGFb|$EvgAi^INk4 zGht23&~-gl2tr|gFoT}E^vK$9P%5(&uD;4ob>f}x`fs3BhE*E z^lU#h95$x;0^3s_OhI_3HkWb6|8{!^`%&9H#Fh=QvzkJ7hKHl1AK`%HJD3Gyu8!sE zpFZ)JXlZQiM3})3{$`AnbY6HriG!me5W{3tyr)I7s&J^-g%IZAYY0U8a_^!cIR9Af z<I*pEE)4^+a zDQxq3E;m&_S%tf3TGFLyyDK~-k}CROw4IJWMt{7xfBjg7pd7ppxnN`K!t1e#bYA7%Hz#qulg?5Z4Z)lqod&k6#aA^5#Xg@u z)QI;>X+UbjvorMAF=P7$kw_eVSF=X42?)>w4a|U1)gR77-6^u)1JclnMbQ*n~EiL;b!y~0~r*vKUruX>`PRj(bA6|vd zP(D$u`Ph$@!>1iMKmG*rqkt!#%IBP0Paq}S^00RGT7;aZCgfyk->G_0Q^tL0aGEUx zx*p5H<@ubI{OaqHjI8D(H{0ks_-67t#pX=n91k|#^Jt@W0SU;R= z9#8EJ#ZOHQL_Z0`6$mFB^7}bkHE^nPSZIDK!o%}O*A;moC#DUJ_*6B3+!YR&q= z#W0PWlE`A*^Rr-GY6Z$QOd|5AY!Z9(oUJm-ytNbT{PsG3IPj;r>P6$kLFhy7s1$ce zt8T|djf=JUQ3s(`dAyl9!Myt@3Qnl}mCyUMkwQMb7{mPGsU=~%n#kKTYC{wGQD9g% z8@oA`sQfcpGE!XRd{;*+C1_!oksS{kProUvA?neW7ScJ1zLd%;nZ!dc04oAY*; z0d}I_y9R*?u0b62DojN%H`_TGBUr5OKshZWUl|cdBMWylSTm;T`i5VDeVKYr@yZ4U zushfx=^1U~W7ma|4dT)ByM)%pKnzj3LOq9%JpW0z1+*Wtw`ydiV;i3)yKc=IHrgvz zvIbe9{*xcyZLayYbF|f3L*<03ftIw1t(czW#H;j~asl`zG>FBd%)4tG{XUP`D}^t8 zdpVa8xWY=W9j%%tZjiaGlhxOyG%B8B& zn8nS${ab3oJJkTb5jYciq1iPF*`xbBo9v26d7ebjZ=^(-*s zQ91v?5{%wp!p6U>hJVwA?OK%<_A=2cx&3Oicbf=HYAfDu;I5}8!mJ&8htiq_!*Tu% z(4mVX>qFjee2UU8Uv)*1e}2a1&v4Xq{M!w!sy+Pi;a0Qv{;5j^BmQxy5NCYK;3G+N zK{yfQ%%I@Rj+Z6xG=3fRXsG9fZ%lfU%hLV=v9NG3`?y3^P`4pT&!# zXj0YZMAa68Z~_mf{nPPiD%yQe%k9*y=>7I=8~nj`j9-qh7D{mqeAL)sI9NHPS%>d# z8=YimQ6>AvcctE%iY6Bu{@-v$rng_)ujaNk?Lcl;;N#TRh21TI9j~`+xNCEpb(7XP zdHnEbVafydqmJavI{$E>*$brZUpT?<6uY!|i96agGf#kf==PUu8S8tJ1IOB@TKH1# zn-_2P5-bOfs7Hc_IoSx7?7j6Rw;eQn;fd7YG2|Npr^#3#aevbe)Nbz_>60x_-bGHF zH#uP8itlSv^WGH)r5|Y@==btI#&|!psZcd;GMCvkJki2eIVn&wIn&H1>BGPh<`vU^ zN+c(JT_K3IYdlAKX5XIYi6XvDdoOkpm9ap{Nq2Dx&FO|4CH18M?LvGhjPU01AIuNe zlnbGrv5!Zc@q{Lpv#R5~U|&t#p@=@lp44~%Ij3dwq(I%9TFhu7M^C=gShC*$-{2DguTyj z9&XcJJd~iuR@wJr)>q=QpOwwBXxzkrn{>ev@fp_2ow)^)s>0#$T2uI!!WYzn2BdqD zS6m8;#0TU}9h}`8?SB@89sv=!-sB(GMtyU9;~d4p;yX z&UdK)G`4ZdOBqbaVw+^~_SUP7w$}G|g^gc@-~W59%f;Ue>Sd&`h|3D_20+m6JQGF{ zCDZX!xK8~}d3voilumciOx?ZhJaNT2COkl+N zuawCdsq!3*Hz6JYG7PNOeh9k)shrl*Q3w-7uY4N`NsRH0YARV(g)EYsl#Cz(6`1Lv zV3{4FoA7x4tdj7q)d}T%Vt!^tUG$XP{ zn0h8BMNY6cp}G?EMGlm@kobM{A5!EB{#>`94rG+E)psKnGTUA8(ng{pWUZ13VCg_E zOrnT>5iqTkx|xE1m$yJj$O8re0pCPVC)est#8r`)uy<|zOEC^%9PWgBK0vZJzUWON#%;d9NcQD>=2;>b5WWdy0O(etexbD|jVkKfk#X0#Wb4|Lb>&F)XskQgBwuO{g@jm)p;)~x`x135#FL>ElAsx zMj23Hn3)=gdAJ=x9LI7|4kHNU)TV|05Aa>0LcHb0RNrg z1P6eR1HJ>m+`(L;>OB3yumd+>Y{Lt*py%wUupd^_&_wYqI6V`G2#4)7DhCcYjsC z^oHAnyIiB1XWU)gK zDWlNGB)bh_aFlA#=N)w9h2Ang42*K>W2j?z>}t&)%_XkoiqSLGfSaN^waNDlDH9jA s4D|75dl`3^crU-FV_I?22LIpr&dlKH6EdU@TM4@w0MPG(==7B7bV_^V@aU`#%5ZrTWY&!VVvPDeSgR9_@`wD+umRS#mGGfTpCe;92*)0w3 ztaBW91G!t$Q{uq-J5nNIfzG`lX$fE(9641Hr#VtN&=7@!$3aCnllFI zNOc9(XHGi1DUaT4!nU!QjKgDeER8G8y#4MteLIP3-`uzMz@9X2_xxT`jNWZ+-wD!& z^3W=&VGrt`@8ffbxyHjo@6mCk-I8+qVp_0CwTd8XI0R%AZ0l^W(I%T|(xOX`J_ANf zm@>1?c6RJJa^c3^E}p#k@)ICPoqCO$^c!&45l0;}Xo$#hCk#8|taHx0;F3A>7A(49 z*-f|HcE>}HKt{O>$Akn^EM}x!vgQyw9GV!B!cx}O#goS7FC!AH^O#NEvsGXR4zLS6 z%sumpPu}s(8v)9>GVe7+O0#1io({tiI10yL&~Z7BO~MphfoYg=%;vp0n1=;egk`u1 zx8OG1aoo*2_uxJ}a6HT#k7^;mOpL4A3*(I@F9+Zm!Gope zX9*E5cX{R{zf6nYjLOPJd2v#lDy8XyLnUDcCeJ(*f~|lK6nM{Qh0|Xm(8x!|8phxv zjKjph5(pT`GEX9lP2R8t*ntCh5MEk&=Uj)O9z!EE#b@)z9Kk#+z~aDUHd$p0cHjUW zPzkdz2lKE1i`Ec(bl!CMVItU-Rm%!)!Y#NBcLv5`q!0@lpqaD-tFQ{IunMc`tH>O2 zxBy(ucO{oss`(*Xp6E@$Ew~ML7z|oRRC1YjKJm*F2sN+l$af)L9Alr47x8zXyhtq# zDbJ+RX}Km>x_#d9%w67hM5e^}49d!4;xD*P#hFl=aV=Q})kg%fM}^Hh95BCL{bvms zAPp(W)l6kImm^~Kzjzv(l7~Xyf&55LK4l>1(tUivuA<~VI9>vI17x<;340zr_V?WN z?||H*Yj4fFyiHd=*ooN1zsyg0#T@5OwRFpiUE1-?eFxFJ%G;`qPyLFwxjXsC>EfGD zu;X9!#m$F5b8$;@n7Nru-ei7$9N>T|c6vNFVE@G;)tm zme=Vg%X^_to*QY2-8r3$xodM>$#cwfVqvOa?qxO4qK*n=A)U3$OM)_#UtWkh||&$G%Gb zoqC&1YMXdc+yohQ*o< zS#}Dq;pnWu&2EZ3u`*w075)OLDqE5b>NMM^-y#h++_C#Bms(#K-69AA6ar(`V`Kxy z*@*ErITNT+Cq@GkX<~6&&LrA&h|$G(dYH@zQy60+6HH}_X|`fIGt96JGg)JnotVvr zNi>8c+e@MDBi%(<+HS^f4#|M50G1PgWd$<%Mm4quV5k{G{mys;24RL)1xI8U!H7{} z&bRop`A&@(}-XTF`{5h zQXY*6^a&GzA(Ijbi6assBMM?6Dq>SaaXWwm4#aT6AUq;yc;Eqy1727R_%NDCh>oNY zU1NX(U?Ql%L{OQE12v!)NPI=ipm>b=m9Z?L#sr53qXZaHHbg`uL`8r>|&g)qpL4>z4%)0SUm}9-+B&uvRgi%T(;%ySO2MHXcd}(5+Aa))H{woO&Wi z?e6ArI$@lG?O4#oTr-!(9vc6q1`o`)rY_L-r~2^|k$pF^d0u{v6jw@*0N?p zJ=Xi|^o)7K{LBl4T*^P0_-bfvp8@d=-Q@q7|4;ruva)9-b0uR%uoAN3y|U@a*T?Xc z{m3>SfvA|cnrcmT_3V0YJ@w<~^8LYZG%ge;)7gBnES1+;4_~QXTwYz@)Na4l>k_!T z(@1>)2-&#=fw0utnAh>hT^ekYu%ni2ja>#L4Vg6&VGe;uV!=Lp9N@)Y=a4gcoO3~r zOK$0N$2|oecxA$ydgK!Ce6ZqEt$Ud-|Bh(nKLP&&JOTJO;Qyug23tJ^$dvCO&jJ0XLwFwOKOe{ozviv>ph(J0=x{Y_u1iofY*TafdhFH zSf8=OGXZY_>$8UN4zRv|2=4;x4~Fm_aNtg1@4r_cnFz}tbU>&dR{H|*TVUHqfjwUb zV7vwNqXEp%o@CIW)APPIO&EDctWW&zACB;mXH?2GFFnjNI0VRI91m0F&T!tbjVc)H zIyQ57Q+bR#641ysc^h#z$j5`1#~qleJ|AMgj+hRX6DWiQURp$05`{J}&LH$!8s7-Z zsig_?)Dw`V^e6e(O5PH4-7{RG)nSjyq-W)t`=!sNX5=k5@{anl9{4rLWznY#wPTb(b$T;5rbfM;ZOYV^EGFGb6-dV zF`Apu;nHCoxEz;kM~pBMh#}fM$|V}^hQO-KJZ|p^G}z$CnUDSJo}42B4lcmxyzwfk z#(M&_J~E@2w?Dq~;YoX}7(wCrDIL+2GNK!>TQ0HcPS4ZJUVF`v^WqFEYk=VPq3Oi9 z{CdOm4TWal;?}U8|4&wnc(vLvuS`l>a`xJis=PZ zN8{MrRxT1N-DXAb^@F&=f>~84O{-Li)c8KR`!TrDL>gbsZqBDGYkPfjB zzb`3~aflC6#xrXCn;Z6YGs#hO;Y)qnJHlh{eK8+`bv65n+0fg`E2qTgy%wy4td;wD zTHORp>M7#eIR&#NS36XZk#Pr)%mWXvq0FZeAx$&rCM6|{*O&1Y z5!^dXh&7s{H=fAlx8(|-8B=7o(^Y;Lw_7M z2zrNcv~vj@JJzeUuiRCIEaO+aD$6)YMCb3Ql;&Dn;>D8TRSnFqbXO(&SFVl?fG*A$ z73ZJDE7elkT-tbKH_XjKXAV0>c@jvw$aShQsxnC+Ln=vmjSEUSD=`YlEVOYa?5#So zKohIy*RyjH5k+Hc3gE6Lp^F=rsIAU+!FBlZnW>hRMN;mvOB<|7u}BGg6Gt~sqt>gy zVX}a?{W<&z{slRdqY3K zf+IqLOinsuCQoA zP4=8X4z6(K>Cz~PJXq>jiNIqZh0-melAdDn!4nI-9i3E7@A1QGX+j4LD_UYKVYcS6 z5CR**lJ`_1BuF-#HrJSbdLaZ(fid2%B6TSy7dK8#lu((#3RA z8Z3KHbaAaWR5P5n+-;q;0fN1x=8*^=#SCP<4zd2ipHF`HIW8r_W$)m@P05Ot^`hvs zZ%2;yQ;m&s%x#v8Y#|zzNdTS$Sq`jh*{sR}*^nnkhPuvE2WcE~9osaqOeG>q%1`s3 z!-ilckfJ!U0Arb{Oz?!etn59q!UZuF8n^Gdzq54k6e}0Le6okg@`W!ZwkF~im%%(; z36m6}A!q)9@%IXA&7!wmZujb)zBMF%BB^tJy@Q2uYtd}Juxh8+KI$T8#(NIHc9Hnm zlhxCX%teg*HfR(Zi$J_rnl~HV+Ko$K^$4;vtWjmPcJbX?9+|!CiynK*R{|_J*zHIjorkNoWMKIy-AxYBjBuphGlSiV@y3 zafaPhVo&^T>oGF}ua(T$`+AsC0+-=OW$2%yrl z`Qz4H^gj3o=j=jw!zBi<1Wv|R!VK?*qJs4v0U*mv5JjvoiBW&<>$*@k8B_;HRX<-R zK5}^|PbJ#-i%Fx*9#X+OulI_*w=U+^G)(q=DrYPQjN!>+&UJDd$VFkUA|wL zzi^BwIiS-UmgFQ^=9C&i2!k!CV6ViFka?B>;r>RHi|r;HdYRvXQ5Sf1F&xZS+XG@c(`#M=E^!-Y%kfj_NdjrGc($~ZL^;Qkz0{~1 zCjnkGi6Z=WA!5vYYyKy);CUB<0VTDu5cpc3K*q^uNjJnWx3A?~Q)aBOgy(@&LxQD_ zoBd?S;yz0~cZc`!gHtVAYFZ*|TYfS83GJgaW{_h2fyjmfYLrBtg-48_kL{yB>HcE1 zEs?^XY8|7$(LeKU38c`JT9vf&B#Sz+5K7MXKpFeT+|S&N;IOD}txm zsM(Cp8c)wBe&@P)^vjgK*NdlUoC^_A#HpmN6uU7JUqESTJYE6I;gt?RdC^2G5DjLa z6jVGw%pq9NDnv(xC<&GwChiG_gh00x2j~WqdDIXwC$T{HG&|@D`5EL0g;X_K-mzm@ zFbx&;x60*2D>!jffKuSzBeDhkCJb;*bA&E1EsYu`=1d57N_K|3U|I$#T>P1>p5g9T zD@`DGd5x;MdowA6+%$Wk5$-`v?P6(^zB>&KT!V$gI)%|V&fm>PnqCwJ@0&Pu4-LbI z*C~gIaUF|KcOAiwHk;iX#EcG?(@Pk_)(%3wGKC-7Ih>anOiB>54-9h*g89WjvM7Gr zW-iu%@z}1XLP`$a|0jK}*M)$=urNp|kSvg{Oq1NlIS2l@EHlHGfu@Zz zhof~}g{5W1CXQ-SIyIEdClf3mVPJ~4N;h#YpqQwTNx0R~J zQ1z0Kzf0)CEn=!aw#d?i_yDo0jHVc@fiVAR^Qxu}D;<~$Y1V66Yb1F8j!1(J0WFx9 zaAk(oNzW?i$wlR8ZdA5C1xR0_{+fUz=*n$IAA9ooOHt=ew<0OwEex7=8-)3uuLJG!lk6>U&`l& zz(Cj<8G4$M%;+p$0y}xJx{|T^I#c#a^oUMiAz?w{HHva|qqSmBi>1)c%D=vb&Y=f) zXvax=2F7dkd^EiV#3Cufgy&=E2%5b~?Fpgg?jI;bE%5efS{W_By}JZ9p*w>FU0ZTm zS+@Yhx9)`_)B-ZEtW5w5(PTd`_}b+VI29Fzl3`Dt{e5yCt-9pvt-@S*1WmriFJXy_ zqg%O!*;{bhWbVcvNKcV7nT+PkOw+09+DD4V_Ml+6KPGB|oJb1S5v4_fX2)oPGkTj zDRN&7fK~g8Wz#WH)%SL~PsV@p4^Pv&m}k%RCj~|aDt3-LDgCyF7g?M&fO8%=E=hR6 zq;R~CYh1IJ8|H^a=iswQN1L+W&s0K3m}Zz;Y$oQ2vo-a(6paq_1nU#d;?M9|Un@>v zpHPFu?_YQ}{!HeEFU^O*K{Vhd<<{tZ3yuNVgN-|`Kj|1%a8Eckhh4^`4%Fz+-QE*@ zXZ*V!F32Kb2j-7ET(>?kPwc!K>cBnJRr&~wt~c+w+f<~jFXAy9MCr9aR~$(KJNhkA z3s}M}U#1@+CGYFc*uq_#I!YAKE=W9o^5q*bXE+1ui!7CR}|uN%t_o)Zb)(FtUkG|6T61q{u#}MVCc|zJDS2tX>Fn? zOd}u2vz@Jpj=+xL-<~x%H~F)X0S0~@P0*4$#T2S*>`hj{csA7fCkV9sTi*Ly#~e!+ zN*$Ow*N%W0>uf#O?Jp)br9+{3R@B!Z)dsNjQhzP~P)5d<@Jjm@s$UOwijLU@N`wz! zbMRj#`8m#w+VS96vEh zz!hOLS&1KURhS3aO{ztC@w_gzS!UUM4=~Sgm~M`V!=`bv8`b_(SpXq(qUW9HCQiz1 zJq~PgjdUHSrQ8Ga_(v}o7uoYluipklg@R*GhFGoiy8VZ1Q33$^S~_>$`0o7j$4EFA zg6ZspReTL{BRNRcE6iNj&OFeI&7)6D;_HcVR_z*In6bO2AQHVE#&f5sZ8)tp~$*ZdVp%}>3;1+Di-7}bpOj@5Yu zc>!RO7cX`0iPxCbU3sCzdtz$M(8s_cEEk(g3cpOs$fVXCJl=#l;7UC`ml9c9eNz9m z-N+)jnk1+ZeG4eZ%E05qgiE9x6>J(m;euYY0T*kV=d_bH8Bfph3s%$);MON1)!kEh zX)L~iDzJr2|0j$xQr&}eQo%!s{=TJ7{2~08Db5h-@+y!XR(!nC>c2A0bs`?CVF9(K zLPChS$_j<*3B*D}#-iI(IV+W1)t4e3@w<>zWLUPgHz)R6?RIy);Xtr67eNbkQ z{wKZ3_jSm08SrXSDUhk(%&rYk$Ozsn`MjtUG;$+Y+OSadjC7^hS64+PN69LIH*DA9TR*Z-RrswJ+P_m|N(-R)J?c04j9y|&6WtLUI z8F>^wBvlNqSxXY_?On(H5@88E9E$wz5R1ug|Ki#B5HK^z34)?D#DjxE$xt4<^4}9j z;6XGp!D#haE9SYbSN`Gdcr=r=gS+wL7&!GZmnh}#JUB?`RG2%eCqagO>jk2I989%tb!#M9vUfA;3X?` z{+{ec`PlJN=Wol-C{Y_4XfLbsF(Evd$B*rUHMUHN&^@~0`FV^~ zR?ff7kiSdc$w0NV80Bs|0JL|~SK2(RUoR#gA z&ED5Lhi^jtD`*0Lq9EYe;ByP=_i`lzsa_Mx zi*xM8fu2Itxr0p&O;8#{s<$aGPAM5NJ`tF?5|zOj1m%hcigz{%dMO|v^<2$NzTI$u zy^t1HIG~t)g$RLUnu*d-&GbW*3>)K_A$H-D38}CJtDmGClj4sHc$a*w*rJnII$Y?M zP0ySc2jRFdP>r7E{7#0gpB&S|uWOJC~t>0_01GqOp_9ply;SYQ5 zbzH(W@EO+k6XcsKPAtp$*&VotP19mbWd=+CpUV1SjSs{Qq@NU^D0n!=B*L4x)~_rm z7^}w3JX4+nfgELs)4YregdMJ$yOe8_dKpL%v99dEeDfcEj4mm}2mv7g>*1f|qfm4> zWShS)ThXsDIt**W&HgKY4XEkf;wGhBC|eto^O~bpj;C_OBL#_ce z_&`30EaeHI@K793kaW?MxW@>&C%CQdIE7EAlc#GJ z)o7GL&@{B>TUpU2t2eea-RWV_t%DqL>JTA#!J&~Vu{j&mh96{xZ>0xltMDS2k$)J0 z;6Y7;PT5D+fa0^=0qolSBy!|S;=1ojKlS~+xJ;Kno||=)KC9dqb4%M!ZdTi>_$=Fw zGI4kZ*h`IIyc&xDd##Z7CY`l!RBlf@pR-ONIBa!NcoDZ57vx*wzILr+jqcv}>_LSW zyZza99bzQzdvRr?_)6c`D-t;6|Ho=QK9-C4SaO7qhl8c#5Vr9R7qZ$#qj@kDOAHS5 zvA1%9?l4Utkp@m3PbA}HGo^VE3>t$ora8aO#=vtI0U9pl@RQ~D-9NXVwfUKF%d24K zWLgAslqrhzeRqAo#HRbg%?~SN8n5E(lK{#rXiY`^xCbPukC@B;LJ_DgRHaA{d4=4B zOzs>r(hnqUQh2N?d5Xy$R+W{z`tR2f)PqKDGX^p!87b|2l#LCZmh4kyJ2Sap`{#m- ztv66O9Cfg=8}6|6JJsfTLT*~|dEZFkQGd7m{tK1UC>WmDX=OEe)GlChz~hYkjNI)G zQQn~dH^Z-~WWV*pfmKa@;aDO1R@L>3RnExqTs>Kuj`Gl_qJe)v@Duj62IG$*bngb0 z`9``|gOO!MRo2qIYQDu>NB8FHek=lzN(@6nVk#mP%m@m82!KV`X~va5R2xP=LrgC2 zed$$zU4w65F{b#CR@Xn$4(o7tD(UIsUfhhV&0TsfiZG7-l5ypyXL&f|Br&n{;ETpB zX9nGR%NplXt6PSv(0+KWjFv8WaLVzW%YLKs{)L``&2QJsoTd0D=Gk#yrZcNj$lhe% z^uji6z=%j={^qD$Rc0svmGc8ie?k0}y2xU#0m=)&>P8T&Y4~6{szmh~jkb z8ej>kI!LMtWj-87(XghMQ7j%p$qnue#-<3HUTJHoN#12kPQy#c_wB_>i8*>@$;!=) z0&E{f%enb8T-%Z)Ir%I!XmJvqfbx^&fl^~Ki|@kyCeNeONP4n72vm;+X@th+a)ms+ zA1?XKp3V|bsk5CL8;3c-1WT3!7f*{jmc0k2Az>paUCWr}FYayVk{lSN(4P%rb6mx2 z?XAs#L0-N9taOej!Dp3fE zZf**PL0>Bu5ENkNX=)h|h`02&^4n$}5(qX}nq@H}GObP~RAntaqGpOLQC#AiIz3z+ z)HbV8T~f^BL&V*4Y^}Drf-rA}1DBq9sDuX-VXlA>Oh~EEefedo7f9B-J9=YWeg6Pw zs~r{~+>&X|1+>U+K~kd*C21{y*JYW%1z(7ZIhLCRd2nAA;&L@ST%6ovTnQGGToKL{ z0Qr~S8vt1%2}p&TM5W_hG>LCd5z^6kmO?zy5|LH@IJp+#@p&|avlRx2)5zig179(B zunbrJmzK(@5P1m$5vy&IS5PHkxq1u zWOCKA!PWNYl)4XAi~yS`CQxmdDn@nVlsfTHR?FnXK9qRvj*;q#UX1R!N&W;m!KcUv z2BG3|S~|&BJl?0+8w-PlRg^TaX^0llZ5XAxVI)z)63!AO2MUKpfE*s9)SqUSTfWyv8 zHGdJ7bpYdZYdj;s)1Deo>Tl`M&&HZ~4?FqTmKd;2;y6kT$l0GHx)b3OY@fY(CS(7a;(M`VE1xbh{id1Hy}SH($mxe8QA#<-_xWta!%RU%gFNWz{pYn z_5Tc7^>N8MpyQE)bHCa#G9L|cm}qp?sgS-4@mPsJX}kUR6E$Y z!cK?hVmAl6M!$*7$Le6uLE>I=4?KPCHz0dc^TKLkQ3e!cNhPL`+4aAMkK_1of3ieS z@0~|z1dRfjIa=65Cz7L>Jvpp6Tu(USw$olu?Gvb;DJQ2A1Cyh- z>$c=zS56s^a;g56wBU?*V2qb6xr5eVd@bFQU%^(%u4O0#o!sN~fSg0EDSme^zYuxB z12+0Q(TB|8Z)F>tA7;gK$1SzO@7eAvxgp%(eD=oLU9~qSNX#EMqk8a@Ua(p=+_pL)!JFi^IToQ0pm7Am)i zjqJxlfZwLr&M;6ipPo)w!e6emC7w|jz`7kmIALpcZgK`0*i2(GWpNwUXvyVR%9yXU zhok849pKlN4E3y*K+>>Nkmy%Fw1jU%hn~QR?Y?VO2)WIIuws-+o3#ffyS=6JY4qt}k0nN>7(r%V-p$mM>GGs=53sh}jaqA=9{@U3o8HQC+ zso%fx==_UNoT0j^zK-Nc^q^Ju41f(I&L!w_t+H@;(W8n?cnW{UUKSfI;u$@J=s?Xb zZHh-{@%0&--D=8LQ6`+OR^huBz9}n4h3Hg;+b?=GQxR$yKRAvx?HkEP`RHnYln$ev zUR2SNjD+ZNO{%05?EUWgJLjUo(Ku&jMd2BsGx>uY3vx#$hT{R1`oA5ZPsKuaeyWQ@ zWDInGE@`Rm4iS;g&Su*+wCt?Hw;PzNYdY8g!&{5gu%wh-YgI$63=}mi%YNv4c@mhFpn!r$Dxsa~kx%!Mn#qx;pv>y5aDy zQK4>*enF1luIf~F&`LVXFEa(umqkyq*&i9ZOD79}VYRbeL?=x$9&M|yPaANvY)6HLD1l*%29UG+_3D06b zpX zNK9Wp8mevg0xaZ=doWs;&XB zsPo-%$NH6C?^{-5eNuXTCx8j`L#^{CTQ%2aO0K;2-L=Y}ZK@k-Y!H(bcI<1J%^YoT zq_-RS#W%56n6w=vV0;V#<6HEM@g1qVT4j~;F0V-NexuwNMma~oxDGUikARysif=W2WUX6yD7s9pyq4=)rF@%e zjl>})E9}_U#DM2EG9%A6c43oA+zx>#r(}iUzKf1A52NzFR!@BI$~%h4dosgl0cHWk z9sSiJw%HXnQ$nV`ycQ=GG&~eMd$s%w8lwrgPrmUs2^cfrTBWFGjA|Z`6fj5qO3XFj z_vK}ir_h003O=Kn`hOJiv&EEgj&44|pfVPtengHAin@pD5P1yD-RGbsU|w(sJ-Hv7 z7Yy{lsvZ)m)L~xwxjIuJeQd?nU3R^51;AaszCW4+)VphE^^-Xc)D?@SLfSnaU!)?} zE-+-4B5PyeXLFjJI=9akeb@~X#SjLxe?ml;{yX|jjDY?NpO+mlZ(XDa^R~V(rm&mY zl@A;hnRz|s7EQZ$;}C~VVjj23%7K-rslC1koXWV5l*LbIh6hM#NxZ?yrN?}$bRm4 zPyc48R_Awt+&}~TdP|}Ky3>`+Q1Xq9UW7|JzWm1}yh(C;)6(KOKH2 zT>`+*nap2|M&U?+?b3e#On;X_w{o*8X_XhpdBs>QF>C(DdVz#qlfvO4rD8d@;vDT3 z)Y}UhV{O{XON`n#N(bw~803}w&}bumQ`fa46k27D76ym%kW_RSjA}<04*PnZ<2BE2 zO30dP*;N|ZxE7|jvRT5e%L^+Pa`lO9O2O3x9cD$XJz-tH+qh}<%g*YdUPld2RhhNy z;OY;UsM##nkii`h@uDtTrUK}>%%cCf;=9#fL%3w2{D3*&71^@#6# zE0Z=(Wr_%=FMbq3{w*tkm7X8-5K6r{wKK5#E=hBn6g%|vJ^XfJ?ww` zQR^x6ySUaGzZ<(-{#hMomA0>j+J~IfE&Tl`F*m9x&H<0-8%ryOp3$0@i+p-Y4s+Vu zRK0Ag(WX@^nW~grsT|9nSw-e;n`FG$^IY^G4`W&jo8YS!PEjV4> z-lW5X+AsYb!zf?_% zH@q_^wgWos2MEXM3ttGV1rZtGNW)MdV=H7m)t%zD!c$|v6@emE3Q9EcM;x3P0Sp1lrV`onhQlyHe3CFq)jlaYNZK`jXsZZQv>aiUk^@4i;j!)#1h`Q02V|50Ozy)CbpLzagWB?_A6~Gc;2k-(o{l}vI$65hw{>vNzW&rp95GH{3 ze~3H4rC1zxPS2vq|g#DXJRe16wf=mM=1$fXk68;U91=9C{6KM$LqX^ znrtV-I~X>tbkHdJo}euN^(keVtec`4^q23cz*}1H9LNaiJbdO9rX;jdb(Obk{;jP= zXclYeHbHBCTp>`6oFKPAfTkO4Pe%pfja^ZVv6Nx1?>y40Asbp3^>31eviqa6{lyGX zN_4zM->^iwY9xv9B(tJEx>qItAvdwA5GCJ9eq1p)6y&3ja-wmv^U|-KW0sA2;)|F-cWb42J|Ze?P2fZiA^ z;SVUK@5OWh7%isM12b2W3yr#6mX!c=9S9GMs#jK5#gpQ%q>m+nQ9UzqN+7UZ72P&w zh>5wFEEb8)1pmMf#um4qht-~g-+5U=DR9wxbP7!iKvyj>8rwvi)i3>dm-y9w^MjlPvT0--4DbB!J4QmkY z2r6{46tfC>dXUI4k+0tHoa4^jNS-J65=c`3ZB}e3i^m7&h`^n71wA%wN?EBPuAIk@ zfMc6aIWy&fZ8{kW!yPIess&>S^A#E%CW8x#4ayaU4aV`7$_#0UxS$=KiDD5>oFGIP zHU@l6TC7RoOo57_i>K(?25~_u)y+qR<8#rSF=lPPiSlE?=sx}>cdHj|1N>#s9fI_= zQo~PG(UN10{5Kx{zZ3p(%q5Ws{--WJYg9h%U<^PF92wTUh!(|Ms3`6iVqO^{BZbinun>b2V3rR>ym?1?DOE_p&|;d6+ziqwqE(6E@7OO#qjJ_)QCoadMi*ml=)XF66a^m*N$@ zSjnR$=N;cCzm%O(Tp{0M60c#qQHD;+9TV!%3C~T%JWZ5@u3oHId$UOj8T&@-s8aId(V>}6LX9C);4L}yQ!`Q|1 z*c6ws#DS?X6bP|V5DwEQ)Zg>vY*Z+RkqqR{malI?eql`SpzcTc{mQ|BGXT?pM6X#(P>MwZtgjDb(9<* zr;Xe)h?Trr%j(~>oQSvNU_-tozGDs_lz^m(pmflGxs#=>7-gK&K4 z$b0LMyl;`3TYy`UZq*Lx?1WNSR&1=u>}Ft3x1nsL+BjAHaz?n!fuodo=C4VARXv(f z9Z3#6%?;V6JIUz6iiO7Rn%OUqz!l)n4qX1i+tsmA*?;!+iZ|!Nul|Z>W?XyyU?k3#O2A zEvJ&~McbXwfla2949PV0uhd}-@##V}OgKMY6MGM^^cA6X4h-F>r49;-5nmy)2Vw1L z)Q7}4Yi?;As61xaiy+#m^@pj+R41m3*t27Kz*B_53eh1aeIdIHeF#QOYyp;oDN$`>69WQ_@hGk(1GOm2?>7~M=l@*@gMPe1ZvQIB{Em)u#NiC#QvePeg&W>e97LNbwf*fcZu;d}vsSTdJFL9o6c2gclxwhGR`xxY z{hk_T8~#sDD0;gHAs0X1CSX{fVon;e0XxH5x3MT!ENbn_UK)IeN!#kv_?-)Xu&=K?Rd-%B5}^z7+5@UoFe zU?fNPktn;uLb-X8I;;gHJh0+<;Tgb!RpqTPfsK2_`wbK26bjdcCii=>%6q;JzVo>Lr|+lYTdxO%Crwy$g?H!Fiu z*J`tmCPKsZ12>c{OX$k^Au9S%?nfxjepLQcN(7DeBGj`ZA;OCSM}#RGYL7b2I8+k# zSgiZ#Mh=1M!&j3_eEj3qEmX_`ZC(bzqDWe>;E0Mj{bZrA?YDq z*(xjMuDFlWgCQ@ z05fox5gtsk2<`0LODw;o`4j1Toh7T*nQDH&V!dty>^%SbH#}7njL9ON2!~g^qLn|> zMAv4c=TRsBds@&GDlX?}=YeAiNzdXGe~`g37z%NG1c+?{nh^k(y0C`I(Yiw`3St$K z;h3VxvPOt~hnk>3be-}M2x5Zk-ZXZTK?n^F3B})FSS?!`+i=g0y0WYe`l-f>xIM>Y z+R?ME{CRUnqx%+C;Jw((g{=9H>Qn~FC zNPhV79nFBi3oOU3kPYNNbNFgmf)asVgD0-V3Qr%)<3{_5*?ITw%}HBB}5= zkv;P92p^jqi5~Iz^n9tovB>pej#hB8^DqaVQkcjJT8N|dT6uj}Cc9{bEs6~PvL<6aDxj#B8Q5q^KFE3kxCWJ7DlpU+6T!jp5r3f!M}kodDWfa zLBxcWJVa@z z`(A2sXxeOM^cyJ@s7ZN1`2E*N?R5q$L0)O0i>3JX;DagoS{;N+9e(#&1(WL9QPAB~ zTZmg45tdD8@;uSIgUnpPk@2Xxy$R(K@o&{Hf)!3SV6n<70mW+Doq~A9QqLRwIGhyY zMi9oe$pqn1nvGO$6(o*TZ$@eZV2=iLYG8h!omXN64LH9*4+K}CE7M_(R9V6IX?EEb zKkl72v0Dx=Y{BhRbW|)6?4mYTz_3eB6P#`k80*apz}y$BnHm3`VR+6>9tr*1Xw@eM zxkoRtI)@(7RvP!69wyO$&X!Cx{>t_ge);QAz#EMF%i;xB)BUq~3fWq1JqaPy!rt_O zw7P$qpoU`kL%T+1px6AnB4q(ejcb$LXmt+#3Xz8hlWW5Hi9SEF(ZF5n43Rh#dCb5S`3w6Rxi;A1ck2|FsLrnSFy$oMzKk-XPP$LZ_mM* zMocQJpYgzm{T3_yd#`A>myUi5n_S>uoy~Wc6 z0NbhUI*5=a>)90nN%0q$xtYYwr#go!lp!%s)$y@4}+v8s|eOlNYt81 z4C1DoFaH4qhyrT=q<1G4drKf}DFcp2l|pW^9$P_}0@_;bkQUulE}7ZEax&09hGQ_; zZVUij8QRIGIzV>!vP0(Eu5C#N-sn|EvxLJ$m4do&))q3pHz{c0;MFUb)CvJ zK*b1T9b`XG>%;>ENjizo_s+l7L%AR*w{TvpI9z@gYeN!cpP^BdBu&e^F z^!kd8bHeleBAM@K)2CwO84QAdE! zz4*Zg|39Rgq4x5a%u1n8e}Sdf06>yl&P+KSA>HSAz75Y5g7MBuU`_282fnv**x=<@T9~g|`|5BnzG%ramQVh` zAOr!x?q^Vf+C@JC_(2y|CNb5Es8QqAL_++#hyPjLvo<{JDLK)O__M`4|U z<^2SRb|9%GIduMCzrlF>=g!(c4}O{AAwW=Dv8W#Y`vK|m{U&3Xn{k4zHgXe4RV;nL zTmxxY$A)M(Zqc)LR;V=&0b&|-q=9^q4&(@OkusPDPXZkCKjdjn=G2Bwn- zn8@~^KQC~~qK&(Y0Z(MTc>J7?W$2$cf-?e)2x9)0se9n^VdWe=BR5m<%IkiuQl8hm zUQfrNFa#43(C>~d`iJWr<=0Mf$YA|wXFzBEJ>0J8B3tGO%G4l>#g8}k^A)E%-=!(t ziCNhlqUAyzKF@#(ez0mVVgEf;XcToYtH;Nvdw=(*j1H)Oap~2ml1FFJA$o&f{PJBx z^Bh`dsN8l76#PuP$!g`xE5N56Z|Y?6%S(--E24Thnvnw_&@zaP0rD`Y=8|lk#+%^f zM&GeDSHs0elIl)uMTa7TBk!Z0oTK_-<8D9sR|5WRp4KSjsK88r zcmoB54o*k-GvbbW9VM0(i_Zee{DtUQJcrO8cYkUmZ1Y6#1JIx6oV*FquX{#QoMW!#C14;85fH*ufRN)hZLIBrx zcM&kjdDET;Mb!;f2dxRiP#HWL+DGgF15>^3Yl~E0iWnqqPj!%+L@)OVUbHAr)A^AR z{NRzKn9zmQUoM9MJS1AIg{^h6w7Yc|D0{}!d6J#aoyTqtDhZuL7LLhdVg{Z_Qh9Txbob0uK%8 z-m6ZzCi-YAsvFUt!#&@{De5P^lhzJH>y*+}G=5N15FV>iWej!jwck9YN&^S$&@460 zZN`I?^2LXkp%W^ammMUOcuHb$Tg0(}SD@Iamyt)yC8$jbXb@f1mhtnVzQ^a~f@Rl%!H)oj2Ys+p9n&pH1UYZ)c;pM&ed3v3$vzCdZ}Co; zhfL9F4~VYYB&~g1sH5e&;(;@&*Kemz@Nd5yXByN z{FkcS<7?T+_~utZt*Sz51Jo|)Pt<;Q#D{STD_9`Ywv}2aB~98ME}==~v7>5C6s@C4 z0laJ)y$VfNK?cEXGUEc$2eZD9&mPNX16j!1z?&N?az95+G58kiI*lT`jw02p*&~c0 z<&k2k{SWMW+!HBz?h{{~pPVa)IosQF3}fdv(PRNU5o_Dp!gB>2bb$;*Rs&4h={bDu z>3GjI9|GDYHYs+-A9`s;{|X6FCLZ638SBCdu2{um$+fs$Eg@3;7DBRL2T{qaulXt# zF`rQ(E5kg6Ec3-xCZTUQ{y=XGlP5yCHMWBvp`gaBmnCqxA=`|GbuFPBD%xQq;+4ED zzR+mAKm&C7;PePN?TlgZ{r>S3x0!43qU>6aMNEIa73TX0#+md6!3tim?&Ph|AAhlH z``%rA2Er^i>`U~9#USbR;5n?z9A@*r;9~d6^ys*OGxoTn?=mGn-g+?I30m=UB5rgo zeaD*>=kx;K0y{Og5hc57-Dg-Id`)hc?sZQS8KmGia%(k%?1eMo{lMpaulo0Szt4|V zzt~bGUUM3*A_u5(S`Zg83mKwD%vlG+4?>(SjRY}R zc}LLGX?VAo3Tro-zQVzBIa2OEA#yaZw~+_8y7&SLFa{2v zDA3A4)6*jgrRtJ^P}~`v5rx=sM;r{spS{O61T8((e10wz5e)XuSh?V{hcGxxpe>{| zMSd&^d)nepyD0%xZNQ{4K7=w0(FR+p%^?=J^uKTT;dyHpP(&QH`eUs|jk(xE`U0z~Js?^Ao{A zCZ(sNyktnsAyG<>+aj)FnZa=C4GVg?+#b0;ZypwUbZol)ar~$L-kwi)U)g6u{NWwFSRnWV+gMw z7m{%i1&xK8{0r@~iU=VBid^;d9MLx0ieF9WG;4#&AFY(_>~r*1p=m@P0vprC;tC(o z2b$d~EIZVWb{pGyFmF=n^t#1mQ~S&eoKY>zt)dp$yra#Yzr(?7!V-&}z`-4HX0hkC z{BRI~;N#TJyo-@btIMpeQE|#WGi;~`EMo3f(A*rEW)Q6*5{BPkTDbJOvI?8008x7F zqLv*A5M<)(?#Sx7xby;#g=wb&h^=(-y?wC;yN5< zB!8BxYt@uhHRZ!3o1sc%Xmaqd7Y394qUDs>wro^}3S-j6m+wn|)!N2J-eV(F{kzjS zMc#~|x!jrGGv#?NHMkEOFo1exbrpr~;MP7A_LP&Hi;cjBa2n8;r`fn7CW0GojVgj5 zSU&R*amddQe`;xLjz8aD)6Fy&2E&740 zinoXmq{}c@C^e&3TW4N&(BX}8A8 z0pTbtQOMs`%p#h_4(B=}$FXj?$k@#gLveFjuiJ z4`D>Z7Em;;LA;$E+s2S3jQIQ;&lPp>0u!ZNTPczhIj$B5b8WBOCGUbgZq z4n8&2-Rp*94uM%(j{W41eRWV%ytf8R5SeOJ{oyzDm}5-j2Fw+TdOJ%!V7%zd5r1g^ zom*IVcK8sQ;{V%b9*dX9ARhvW$t-ieLzG@d{?ykTE1GXATC zH(QpLZE4uo(d7SZ#j@*D=IR2qX&@H6ti#dhH=;i&!v1Vu!9p;mnHND+{MN5jX#zy} z!j0=-mDrIL7&V%_{WWwHcJo}1Ra-nq^;CGJbnl!AuEH6pt zy7}aNvK-k)))as=hddkkiv#`1JMwnFSW^qv`BY~SVEYB_c{~AN z#>7CuLa~4vt2rt$AbX7en*?ga9HCSsOgtZ%c8$iUYl@m<(kQkudQG^wIJ`U@y=l@mtX;(>8( zuFT+sqgFmZRF#CU_U3A-R4hS=JM7Enzlt5?u~g%&?aOp z2YiL|a}UwU*H$trP6+15!XZz&9pF~G)==mLCY^=rk|}rb#O(g$0HxM%S!4BBa+Dd;CPOw_5HYq(zR->gB%b%`KZT;}Y{ zTMSZd0&iOGg*hd-R>sVP=TYcX-PS0txHrj-W0VFsDj;a;pP$Te@}$@dxx#H0%`6-o zB4xt70s#@^84V26YCmK)x7Qit4JrO@YI04S!nfTAs7`~f@M|xCWe!KKs65$FO2oW1 zV;GBufyE*Ks7U_lpukx%Ym_TjltwWbn=%_wf+M{(m+*sGjKR`2$E0G!>h^+FsL7u& zdvAMzxu7tGphrU{wC8ky<~=1iUEhy{bE%Fn>ay0YE=9?dZV}zOcHjdsm;9FG3D?Zv zx7a#c=5QT-`ub*1SCUvr&NIIQJ)aK(`@2}b&y;6Ii|Il~;`QP*^R2fv6KQ-pT+z%b zrozYSA(T6@pm~FisTo+m3xT zOU#E(Kyj6W7V(g6NWWq)v)c7+ARfWD{m6NJc7Zi&A(!0M)}Kyv;cr+N^9cJ1vhaRr z;qd?oj@SsSH5T&>N!_dOEURq)^(^h*$EU{3v?mf>9u$cV?Y!QZ*J9NwB$>y z92}I(a~juGb$~0ZrShx=u{6{_W)*I`cAnS9R5f?j7D^@}S^Q|=LMki))7vSwwA4%n zyuC1iQ`icKj!HQ^4pWJb7x6i|4%id+yAkzF7nSx*+jje3R_YRd4E5kIde34S^vM!B zADLDCjvbxP{lZKni;$e)%AoSM(A&gWXqnLp^5Ns|6V~GEunm1It;G@gC&VZUe?zgJ zw?{s{&2Y6ULv-J&pnDIY$4G-QHMn!E+j_C1Q$$!WdzaFvLyrqrhWFv%Rv9|?qp8d{ zI0wJvHc3}Ho>J$)4n0crCWBv8T|d54e%{t=A0JV&Hv&;KRl{fz3f2YEa^?}Q?Tq#| zXwH%L!=apekm%=a^3E8)T0KRc366CKxN&IukkCjfq_meAkiB_V;*qy%|8$61dlJ(! z^@0U}e4rV|F@IxJjbqLttb~Js>#JCyA5VJn4bmTA$t4(NKFQamAEOj0xdPR0k7-47 z_$MDCJ~VNyZc!02tC}@MdqgL~U?Pr}fFL4h3LW{WGv;3!7?NoFB!6piV>9aRPznod zY&mx0)FQE03ktCwOiYl=gCm?!zBsIw_$d>kUi7|tbB^Ij=dwrs>Qwyeh;_|bif$3g z&}SaV1X2WjNOKA1nzd*?Z=mTU+w%%t!2~D&iP@kpNEt>KhwaARHB9f6Fx>!i0ib(U z%GoT9Nwi|)KoWQb^#-TGgTsrm^~Hg%2-QCyrtAkp7*>WX>Z5oitFFN_;der=xXn96QpV)8C;)#+rsL?Sx9f~)5Q{#dT zv6ihvEvEsr!b25Dvp&Yo40p%%KEJc*N5$tXz~=Hh;CNVonjjeUWP_2lNyGo9rzt1IyFL9BLVKK`j^EUuVX?px}CD* zTxvE$yE%(idP31%u$a5+ixo9P@IU1$I%(?F-m+i4o^^(C7o-?VRCNgw;>`^5emEceDr9z}15!f_ff4CM!8PK7}gPlmg_|^Ea{# z9`(}ub_WAFT+&fwM$!v+kAEAvFppB|x&rq`_?EHy8sy2`a+H?t(w@$R@|KI1h2fYd zwTtRf(`LToWHfwJ>RDvm19B2r1iV%53Sdd7yETwqH6r}45WH)rag2lFZekgmDdQ12 z5^L{958JlCm*qEv zs01+Nou_SQO3!_sK@7#-;VGky-Ky5SM*R-T2iM5Flh=A`MAwBLsgJ>}o(HobLBDYn zUlHKwVix_teVDH8K9N+?$(sGIGIk^reDMAy>6qy<;-olS&0#^)SL4NetuV5S8&Zj! z;x3i+XE}AgaKzC6Led9h#n=mJ#6-k|#i)<94(!}LVobZ(SVpwjg$~Z5KjH67Q`v*c z0UQ!?z)~tQdNxaOJx?aR&x)!ZKr+mgCpxJ(tIoF&+z|V6-h7TM4U&d-%`E}9QewqR z%6(80h||B|ug{_%3GrR5a7M2~)%UYK?!J!@(p7z}o_jdj3eKW>LqGl&R#Hmpg=1D| zA^JEkGpO60jpm(@$AuoMxNt-68ehV(p##=!xLFD**pHQvQ%PIIYGs6QPZT;pd7nq7 zz`3&WX(DaA{n_#N;@Z%BtqKL($mbxF%$kT;o7@+pb?i`#O2QS1=DzSNovRC}E-s7n zL@w%`Lb8tOJcMHSeFbWTDb3#GE7N}|qpBrHlBx~Q3fo_dQe-v`UWlqNNqnHWbKFC-WiZu%N-IgZtNR8?k)ImT zaXe$4u`dOb1{Dc;Z z)U3nSk0f^0Q8`)~^t9pNO1s_|Itth21xAT_l6aDp0Tff0e@yg<4)dmuLKswBBRoh2 zLzs9Sl;+>3&e@Av@Z9<#m90!r6#Il`e7v-w=KSzMSoWD}ieuceFdEcBI}$T-=&F$9 zCn&aSz8$n$s!EJtTR&var=VS2E$!~fuR*e|UTr|%Jh0A4GGK!rU-)(`9seM-^jfIh zj&PKx19boZNK2fw7(Ad#i%CjR($Z$<=I1NphvVnh;|d}a=UX`aZ^TAG$E*z@z=|A} zNWM_)@;kQ;O7iBz!iuq;8)eUm=fdF{tAx(}WA0WYL3UIZW)blAz^zr-2yfOEX+kk7G;CXO)eGxTqu1CZIv7;|yEStQPg%Bj>Nat&@RNTTm&LZ`|CvOxa^=~3igU80}z#~{RCqLiSTn72n|kB4Aa zVKM;DY%xW%Q?8PYM1PaLB`#()y(L3IaVC&U-kNgt2sNH9)HI6=E^08}N50BQ^S*`x z9%{=1*tX%x2-7D>1Ygt-PesCp%U+`@^9iK|7Y0PoQHMO7Zwh&nn`_{~=a8y7Uu)CI zst|8iX)k)0>)R|@rgb4jqnKE4_P85-u}w_UKkQ2S?^nE+fk7UWvuJrPcD?bJ5o8~i zq2Di;e&v!1{h{~cR^P90lxW_LMB^sCXw>eFPD6cv9R#yVI5I3muY1;7hKJkSdiN#xeXG3!kwVz(E&+#Sj~qHV;B@_ij~oH?Hek}e zd>|1kG`;a-glF ze^%6y>GBvEgW)`ew17LVG>{f9<^Y8YTd6n=P95c7FqsM80A8znaiRH05;w%QM6#q; zhkXrfo9;&59;m|=olBSY#%?xGChrk#3OR^S%peHUiCc9{Q!0X7L*wvsPx;aiS<(IV z8r7w3L!U224gO_&2SG5tmtvnkAbD)aESt)S(GXNRJsqDnd_4UL8Y-#>h4PxWtK6^W zH6x7tv3w^21z4@j$nBEZXAr-*T{jk8~aI<#vSyeML-IJ-21ekB7mB3alHtN!OfEVa^cVldo~=D6?H5ha(|jq6;CIXfIOQ zy?&r`r>&9%mrgjS>sCFw8M_KMX+DhNcX&{r9)u5xH-cbFE-lOZAL7F39v|*e^S)tV z_D^3+ous`vfJ?OH7iL(jMFCx)>i}Ag@ub3aq~Y$D^%%+JM!P?V!^W+e5b^ck%^zkW zw~g6)=k&8gv2a|5wnboqY?X>0ux)3ArNf8E_v$v>I4#PHM7iZMmv~;d`^t5Yfd%e} zP>ET!T2^1$+?T1&DCyqQlA*}C+qWhm$>42?H6y$&_R$s}Bsc^6m8$k^q+2xSPX~L~ z{S!2(5cH7v&(aFuWr8U625T+H0*&jkHfYAHYrpvs`#c5hms`K(tF?dr+ zGFR}w33g7LNf*}X2V_VBU)#VI40~w209&C8e`GT1bW>BkkgE4s!IB7UU5c?E6IP3J z&{(Y0(BEIzzdv1?f?|J!28)S-1CVnEr2cTVMC?K1`@x3|N_`ziQBacr?GdQ&TEW(o z_d0Q~)Xkotju{s;JRZ`q3W*O);(4f?6;;j%k2ssg?tRh9;c@7U%0dbKWdF^=P17*e zXe8hXydV6h0xPdc-Gm>d$R;*^vY*z~bwXCy^!mt-9yqv6xR@3rE6EbN^?OV=8M*Zx zR+b$f^vC(PUm6l+hzCZpO;{7BtkO6kwDl_WsM^4bwO1dJ3O*SFTPQKv7a6)UmK!PH z)Fg5k?jbFHD4=r5X_(0NtERnIPK4jfZPcao`L9eLFuH?meT+txQZ`* zCPR^_a--4d{jIh?D3EeQ*=nw&5nYhH2`k)NTPOLWrQ$ZSWDPsAxi-j?Wzm53D%$M$ z5Mx`OtZ*#P8$XBBsWEBRj36fkF<6%KWgX4kJ3f8EJeVS0Vkv*m%}#O56ikx>8{;-+ z724bX4P6!`9iGkKq_Rx$RLoCELwX~vmOat?MaJONQh3F1!a_`^@rBF+0a2&>qSI)S z=X%KeGuL6V_L*qc9jnL~&w313+E5N)Xz1;N4%H)$EU>to9W zoFI9f91<8q9ujHcVX4)F<+n#q-2C3sl`w$;XBYVoN^z@=hMzlQESZ zP*yQz(G$=S$D65{lAxTAGX7QbpI_;#bNfbopfA;DMm(oX$hTiF%asB}X6o zRBB1)hzY4eXa{*WE)GgYH-fUx5*(G7e3a+o2!8s}GyChSggeweK5RH)s*haK00>)& zLkn7K%W29ljEW;ye4KmilC|Iy663V*6_>Q`v2?v~!$l;O%E65c6jH?l2@x!E$W%Dc_CT|M7W}j!#hCDO8V{*Y%rwr4WE6SR z2w|%DLg+FHzdDwE6CVZ|#&!+9qnT`Z8Zj|=8%6e6kw@cMp5$8{(ga7dU>s&c!rj*N z+}~-Ika&}iZUa?NvI2=7uAn-YcOGUxve6_XS!e#=Sz9XjoI z&Nu+Jt6xg~Pl>M0HhQ3C>1JH)I5~)_*C}CvBnqFZjZv*!>gsri@p`9C;Z&o%m}AH2 z<9;W)u6C|lcgK|_7eVZ0oJjk!Z{cd;YyaJ#_>c9V4uRC)NOIiYf@E3Q1i0jm2ObTm z)aP~r#uPqg4|eSOqg|^I-x4hq=c`YuA<0#9IoqT@lf`nAJQ`PP*wIEyhC;s{n@7B zKnt*v5wUK?jKQ!~*BD?1=v#Ihgcr*ZK880tK~J0t#}^&zPg)D3ab_`^K(v_cD;C#eTAf}B=Qj; zI+*KnQIq~O-$S1fyn6>o`Cffcy zhkN_Hzo=bDcL>m;MdE*3FQ@nBcuFZk#@M1FCG!;%$6KWg(l=;n@nT;nlUcW7+(Dqe zJ|Zh@b*73Z*+fhH+i981>r>$wHA-=dBXu8fLQBmK-tdznk1gg(XH1SDaOMaT7||>p z3=16ehDus=^Nq~#n0e`F{_#MHRQP~w(coK~jdaT0h4Vf70GLm#oMfQ3_$KEbGS0_* z%ZWWGD&^ zPN!U>&XLKb%X=|X^ga^VWFbboDYEOW9JSRglS=$jxPHNpD%L-6yz?4e8`CrjLfy=) zCU0D2?C~|)Vg_;S?IrN29(K@D)%OLdxLvzB4;C27Ao|{+9`pXa94znkt`}|2lw9*s zgNJ}GQ5#J6X(3d_g4Gy-afqhSv_>I|WV|kjR77%Dwpy?c<8!2-$uBeWfj4l{RMl}m z$uxIdg&#*}gTLR!2}DLOu>?0xR;nVo%*_2Th*_0@_ zCem;4G0mA8YQHW$K+StaLo;%U<%?CZqFk+<=4gP}dRj-jI1O^x!OhzxL1hf=ZMbmB zf0MwFvefR3t6K#qMUWOAbt!K$SB{Iau%<7#M7}RrCOk3D-jc6L^@SX7#B{_^+H!87 zzMem--&_YK2e<*m?QxJjJY@>vZ_(rP=|uCCC2*Cmj{DGUpxXzH$)IPcn$|ON`+{cD zw51f7V{pm;S-0L4k7S|=uYXas{;?|W59lP9py&TPEq|LbO_nLB=Ef_itsfx+CX*BG~QJPTSuw8 zb6wcHP^^Wu6Pt_B00^a8;7lp)Y|5>r^dz=0!Mh_-Un$Bd?6b!-9d% z_SxZk{S4JTnN(qQgXHL)RIEZ(gF>w^V#(j0nG6$~GDh}UBN_W3*F`v9aw~mQ9mPMW zdXo9v*D$-VFn=JAh_C`WgwpsB54;R?3f4xqh^SqM!GyFdNpTbvPORoi4L?7tC4de# zp+7slnFc?&KD!HO*GsW6V_F7{%V|kNVALI*BdU`RBC%*Q&E~tLQr*@LKIN>F F{vY~qss;c6 literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-500/Roboto-500.svg b/assets/fonts/Roboto-500/Roboto-500.svg new file mode 100644 index 00000000..2b989161 --- /dev/null +++ b/assets/fonts/Roboto-500/Roboto-500.svg @@ -0,0 +1,305 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/fonts/Roboto-500/Roboto-500.ttf b/assets/fonts/Roboto-500/Roboto-500.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8d6fa9240c90211e79fcad94e003c5d37ac23f02 GIT binary patch literal 32580 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 F{{T_EoMQk0 literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-500/Roboto-500.woff2 b/assets/fonts/Roboto-500/Roboto-500.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..6be92c71776f48e17d2b96a21cfa93b3ed1a7854 GIT binary patch literal 10248 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 OuMBQiZEso$5&!^o%n_Ra literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-500italic/LICENSE.txt b/assets/fonts/Roboto-500italic/LICENSE.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/assets/fonts/Roboto-500italic/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/assets/fonts/Roboto-500italic/Roboto-500italic.eot b/assets/fonts/Roboto-500italic/Roboto-500italic.eot new file mode 100644 index 0000000000000000000000000000000000000000..2b253af0d391527560ac31879f3131e1a307d15b GIT binary patch literal 17584 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 eBuxoCb1H3;1}Yv5a} literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-500italic/Roboto-500italic.svg b/assets/fonts/Roboto-500italic/Roboto-500italic.svg new file mode 100644 index 00000000..43c3be61 --- /dev/null +++ b/assets/fonts/Roboto-500italic/Roboto-500italic.svg @@ -0,0 +1,327 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/fonts/Roboto-500italic/Roboto-500italic.ttf b/assets/fonts/Roboto-500italic/Roboto-500italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..28d03db9922938c39fb7b2dd5e2a9b3823151f4f GIT binary patch literal 33868 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-500italic/Roboto-500italic.woff2 b/assets/fonts/Roboto-500italic/Roboto-500italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..382866ae2b8a74f354d6af2d27350550a97d03e1 GIT binary patch literal 11532 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-700/LICENSE.txt b/assets/fonts/Roboto-700/LICENSE.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/assets/fonts/Roboto-700/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/assets/fonts/Roboto-700/Roboto-700.eot b/assets/fonts/Roboto-700/Roboto-700.eot new file mode 100644 index 0000000000000000000000000000000000000000..f89cad7b7f9c64a64c33ff2d27d52be1c28131a9 GIT binary patch literal 16208 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 z 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 N69Ue10BL+3|3BR=Qi%Wn literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-700/Roboto-700.svg b/assets/fonts/Roboto-700/Roboto-700.svg new file mode 100644 index 00000000..fc8d42f9 --- /dev/null +++ b/assets/fonts/Roboto-700/Roboto-700.svg @@ -0,0 +1,310 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/fonts/Roboto-700/Roboto-700.ttf b/assets/fonts/Roboto-700/Roboto-700.ttf new file mode 100644 index 0000000000000000000000000000000000000000..19090afb10e101b0524a4dbe1f5af074dcd2a1d3 GIT binary patch literal 32500 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-700/Roboto-700.woff2 b/assets/fonts/Roboto-700/Roboto-700.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3b2dd4e2082859a8b0afbf8b5d92b90421e4aaf2 GIT binary patch literal 10276 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-700italic/Roboto-700italic.svg b/assets/fonts/Roboto-700italic/Roboto-700italic.svg new file mode 100644 index 00000000..c71c29ec --- /dev/null +++ b/assets/fonts/Roboto-700italic/Roboto-700italic.svg @@ -0,0 +1,325 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/fonts/Roboto-700italic/Roboto-700italic.ttf b/assets/fonts/Roboto-700italic/Roboto-700italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a20e38894c0bde3942fd06f1905b775e58fb62cc GIT binary patch literal 32808 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 E0}ar`vH$=8 literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-700italic/Roboto-700italic.woff b/assets/fonts/Roboto-700italic/Roboto-700italic.woff new file mode 100644 index 0000000000000000000000000000000000000000..7a0ae05e17a6e8f1b76133875dded5ff6aae52cb GIT binary patch literal 14708 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 Ip|7d^AL*HhJ^%m! literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-700italic/Roboto-700italic.woff2 b/assets/fonts/Roboto-700italic/Roboto-700italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..91d2aa6aaec159df2976305a5f8edbf115173f1d GIT binary patch literal 11492 zcmV5b_;_(00bZff<605Kf+6J>hC9JwCXC?hMs;Xg}7h2YwHDBXi=d&ssno+BP3$$XqHiCvc`-S&; zag3ZG3|7wn ztS=)Jn?g;nT^*IWLN!&SYEo5MjjHba+g7$S`;RkYW1V`@iFaJr&Hpn4{P_$vEN=(a zfHUkJ1M4ynQK_y>QZdV#9BhQmDY=+eqTZ%xQ|I+*JF_nSHXw?CS z+4+nad-RxW0VyfZcSf+UxP`%Oa2QU&5lDtiDAbp*p&Yb^zVI51!^kiN7GX?G3&C>O zfw30`HEhD@3N~!KL~mkp4I@`~8&u~18WKT9h-d`KQ=~+hiVZYq(WXbA0VBrj*mLB@ zod<8eI0Ch5)1lLDd+fE(eg_86P)SP5LS^k&Q-g2}Ld@{JZ;r;$Kgh_Ulc*zEqQpv9C`& zFMvuF7ba@Guzq0H^#xwr*8=al$G5E)D^9F~3Tjqq@l@6&HFZ&U-NvfEu3OcAp&4Y4 z0TO`&CJIy;`_Y(fD-aQmPK6F#CLp~Uh0`a*fC3{TE?2%f*76g9GWjy4UK;cWv8pF* zYLP9&4hKB&3jJyUr``$74YjD<%(~$Al&QV!V?PHt=&sZgqg-W-aVBgMHU*}cVV2w6 zVUBt3x_h3d3hgkXvK;L>j0f@vfi0uni*z&8k_pS z7CRj9qJc@Km}Z7qpU9(LxXm5rnCGsKV^ui;+Ucr&;{zY~zz04=_yTOIi!F9IWRFVW z)jFRN(_oWeifLw;b+;?U9p;$l?r&h@1UN9BpUEVNtuL;=bgjjXw$zQ{sxDgB=WF+g zx1ZiYlk^(@>_1s+jeqfh9djYC1w~8*N**oT&d6rhIZTGo;X`ek3 zrE{}AngoS$a=E@{8``d=_?A)fXP)^L9fy>=!-Q}hUa|4Tp7++Rn|je#Tg2G$qNqMf zFP(VZ3-WfqQpQ;%u6RG*H&#r`U5W=`tcJUfQL&m*sZOKx)Rh#kSC?JSQ(0~q#aUY! zF1RH)KYdlDSW$NonH?79seRxt$W~oBA;YPbeNDX^r+v}2j&XO^{9=vQxoVFbByU}n z$H!JlFL2uLxq7M}obQ2eo$;h0J0(3F#A*s(r5*jbT_aA%d?pEkY!E~&fC5$(#1cl} zCx|>{I91vx^cWLh#fCULCP|K=6gQrvc}o^2Tqi&fIkn$W)T)b;-Dat~}lb}F?6opd~q8t*aQiD+!)drDhgk&3O!f2fkZ8sxCv{_X8qA?JYq3Ddn zU@SgXVzHJ0o5b%|ORz7*+-vh73G++>KDI;=iIA8;i3m#K{`LjrHf-%k65ng?*t#GI zyA2^?ha1Lgf@4m>oX%e(BQBx3Od-P+6Idogep6-%y7f+pLTxo>p}3a5z!#YI2~Gn+eCyz1*m`s2!IZlM<-(uqV2E)5&rN162N2~RnygeYtP2Sh-oMd(=+ zFhEMg-186(B1)vfs%*iuX*e<}c*?2nGC&}CGTtX**IXGL3Yc{*5kN?cXeblO< zvE)D8*gRq^9>k-qPRX!kq-A7f^kpn-8MXbgC;tBjOyrpeOdG@|5@I%>}R3Re4i;lwff}V zb!d;bz)paD0Ot@L(z~0M+U-x$l98 z9;rv zF1DLU=Pm5a5x1dm**W&3MQmU!w6Sl(!cIAk+C#qCFR_?_+sz`6fp1`#=eqE^=9vS< zXc!;S0+W$EW&X3n^s{mAo3|@L2s7(z#8`I8IdbGIjVQ+8gfXUOd<-a3+$(M&s+P32 zeowzuU0>{1dWMoRD&7?@4C6?t$!+mu-!Pj%X3$oPY`O>xRP30{Y-o%jqAH>V5ol|O z3_Btp6JQ1~7@4wQ@uIYWQyi|KE>6|2PV2ZIKk^vYhN@n%!SBes54KIN1@f69F2K&| zmjN1zb_U*s*`sy@oA)uW(AUrJdenm`!@56OK7S4U^DTy%huDnrtkUpOt~d`UatO1> zpQkn!XOy<%R~yhw10|^|sEkUyjez$vn{8CEnyD-Z8;()EAzPgDfwTEbUc~2}&Yq_) zhnS|kz+SRUtBzLj=6jDH!wuCCaS?{qcfD!%I{AsB#$A@@GW5iqJxM|Z_jy8~Ap2Za z!HY9^k-sU=k3%I@J2})_8$%+S_y`0adOAIU@i>`}5a%x?Ymwgv)8$!MCC#XGRKer4 zobYt!*3SD@D%DZ;^y+*dgHuIN=cxWt(7!WiMVo@Sy%^_V-mTfco{o@GI)=wvRsF$@ zYzSr6Q(=2+5X9%QGKTJJu4q>p#Ml*J38!|F+w>nV~} zHBXY3Da7(pI}6S{14tF|*h?NBy3Wz%goZPeF3U?ZYVGl#bffaTm{N89BuzxT%w|v1 z#pg~X&#_OQV)BB=IAkzu*d`C*&mV~|9>T3LclnZ5$g2aBH+cqTt5+A?C`34J;x&Y4 zGgY5Tz{P{v3)eHL^CaZHiIQ_I(cwud`S}N+K+~n2<#=?`MmW5qgm&oY8T!#}#H}E8oDJ>+DbLf82L`#NJ)3;a6bx>8O(Ck#V>eg~=&smS# z7q3$}jX@wg69nfdul=DNVdD}|j(U&euv<{Jy9y6lhZ+<)Z@P>^E*m*VL+Zx7PWB+a> z^MpbshsJlPo6G!B~eP3Wq^Uhr9>` zgE40d>dH6xa2I}Vj(UXZUeGzN#2f<8fN>|DJsJcyz_O32HnfEsDU!^v31}|yAB@Kp>uAvq%JVORK zjC0MX)%+tdqP}E{GLMSdMIdQ}Z#$5MFEJEnUS)U>4onW%d9Vyc*JGxacjdzZ3GYoC zX}~}wJ4hmSh#lj~+Os>#dgxfjkchKT$R@6iiahHzvC;G01~j|ZZOW6?m0j3L)AZ8p z`E~Y#|D}K9jfel~Mdy@~fxrv{C0QHXT9!D1Y3!8w`-`avp%w&RK*5*wwqtz9ckA0q zc&Tt#9vK*)LvT{}HXg3EM{19zSi5nf31Z-oNff=RBhovuhDj`?S-jcKfF`e9`))Kv z7bjw2B;ig}t5SwjwN3~=d=y=N*9ABUioIfIx<S(b>Lt?UDPND#bbzg|r_N^sb&` z0L3mkle{k&p!M{nAjL3dNO@T#SMbVs3X(9AYJsacCg~fW;ot zm(@;6q@i)d=^wzlaQlzQqpkj1{M&uLXV1w`vMi$90SJc*4s6jIyjs;u^bcg1rP4Ksb*1kSK zbS^5acF$}egSsfjInT~;H6%;G8$Qgfg)tMURGwQy@2P1+?bN$K>JJ)Po!bjMEw%CC= z!}tiQTa$6<5wFotO?)fV0}FGP1Qo{m6vhvUpZZ0(A`b8z-&|kl3A4k1%-mQ7>e)jo zCO9&x&kmKNQrI^{D8tRE4X5_eN^I0^B9XK02*oK*!)c4rkVN4>j9 zB_GYI`P`l=R1A4LNoA*@wU+(L7CW0m6qmS-*3j*@pHbG=T2I|q5!LQMsf2RkT0^L@ zqMueFF@%(sdhC2iGS-65o*~px&fRNFwLdHFkmb)*LxjEN z&!i1JFPvj0eFgU7Jr@UzJMLzCDd=4#JcmJ4Pq1lnL%%oK&sy>f?8kdXc>5l*Oq1PT zT>an)g78tgY&a0(j;-_e+#_AA5`NKzqnRmw(Z-j@$BQnMdo0|R1ys2kg4FOp>rO^# z4(SRKY*EPo(g|&@wizaI{DE?}KS`cc!a(6KImLJKU|iW826@{;segb){ddXV16BC> z1IL0V8+WQb1+N6pq1o@)V_-+NQ(|c-uhDvjpF`KaVUK`4*-n|calNEA30EAiBTkZa z4L7>+#aiQ2O~fg>uAO!`-5YFU({azxxb@$HZ<>PXDVdx2tF3L(r|G^z`MFpyEU956 zivv>CmdC4k^Pl6*D| zXVKDQ>Iug6Uzwrmoc)AYBDZz?dt4KC8Gd~Y&x%3UpeRom3hUBn0rc>Y*f4AefX!iY zJf(L$U41tHKgC>dR6TuIQN6WxBMc+7g6`{Ao=VCuZwS`+-IRNFhRk=5*3|rZF~0%J z0~1NfmqE5L^7V^wgyC38C%u@K8o&*LvCz*27=67c{3*nnzcacZZx_mh+ln0?Q!8kV zEgwFX)nJWirYn%b*GKhJ&z>0Za=m8@(;YZ;KSvw}x~19!J1jSH^FE3%*~y&)Y)AjA zTmIjqE;n{c?)_U}h3hcxEM|M^dmJD}QLiX2-bZ6_`6=}zV}+UDl0i93%;#Qs63NBR zp~v?`TMejQ9;t4S1Iu#hVN@^oBoF8hOR@tWk=?99ETJ<@OrZLaUCpDcp%Y9_0l%#C zQtMzg*j4EIj8?|r9YXa+0ZK{mD4P4sm_gP=E2@`9!MOtg!on+jquAf!thL$5zlfD% zrvrl>(rus(NQ==wPVytVxCR5)aN>j-RUS9e;WUk zI$2{SQE5SMB}r=?Fg;H$^*wT?W7R8&8b=9_I3Jx+MeTG z#j*aLf@L0h(A>`-xhT*7!qPs0R?vdu2JCsl`*5|I!lni+8y1X^dI>$ z;}}#}MNnfr@j|?JBkQRw&I|u~x1WTZ99PZAcs}OekJNczm?Z4tj9f-i(6Qji}iXJbx72>;|%iRaM zH&y?%ueZ<`8s_8o8eV@`I@nDL2=mPiUj=`_-)Y(@#-rs(Z>iuN#AL>-!mIy1L(A$n zAYn@!gTwH1jNqW9J7X{j)`v5E#SYyBcL8uHeZ%V5_fD{20BgeLit?68eZ)M|#GMjp z?t|&q(Q`y*(wEnP+Qi~?)Q(?#Dl@<428zbH@dY7R1y3ww#+5B|5}fSMbh9*0g5 z?zl2dD0>Ue7Q;3aW3Taxs_;k^w9CgQ>m($Lt0y1jq#NlZ56W1+tHbg@`}I?{)T7J% zHLdsGrq^P#=;PRPRLQ>o-&Df)|l5dm&>URJ1X>ozN0jvdkSYG&p+)p_aoIc6NtYQg@l7c9Q(@tif6NGse zh8kr!btD`05ofVAA3!Vc1LA&g{RJNw1lBFSIm`cl7CwK5S-rVZ*F*AykRSjC&w?kf zj`#ihUJdU<+B)Vpa-NQQ;olR--v^apTd;@R;_u`E+?(RfN;6NNaVDNh8&1>Hh#m~e zg+2uc4WG-;!7BceIVH4SC1b{!r6-EW_hCBh$YDk^Li`d#(N6%hLMw!_YK$*By0o*1 z#M~`sNpW597DB6VS`H5jNsNTyur-L`E|9RpguBr}R$+?(iCofh=1+Jh)`P9&8>i^U zph`Kw`0x$I*8~DL{QNLO?!5duzkMpJCBV~bm}+rx?XrXDSJaKYD2ZQSwNbS$$+I1O zIoefla6cg*^*4!BP%SQjzhpNVcQFRq@KO&wrI58G)CL+5qIP zUV+NXx-uiW>3@zvl~s9}GeAZ7LM>mVR;}!lA;GsRLx3_gEGRgpCIo0%TkmAqCw1@s z3DtqMUk1KiUE zOkLHSM_Fdj-;!?12E-ob#W8piUBAT|awoVzYZ>h0ST5ou%AN@Yy~vVN5}bJrm}#=5 z{{Pk*$M+)N3Z)Fl+6g2Qlv4BBQ6D_=h;UhIjWO9%oqC`gD?n};P6y=U^U-yJx)ykJ z+X+Vd^4XCAF}39Qd}J0jioK?wM+#InNlp7P;RKq01J99s9Ks#YBtb=$nSksm2f^A= zmktz=5vwer<(KCVUwTeG4@H2^X!pw~Tb&aiI#m68EZ5zZK<6J{gV&6yh@Q zD6D!1zy%w?gX;KCv?fJne>Z8Ru?5{g^D5%6`0_eYOk!aW>_+K%B?E^3ucNT-`A#q= zazkOcpN~bilPPeaq(}A1-R=d9c=7$OM-RhG{sLmpey_6!aZYq6rq4~e+c)OVN1!Jq z^^2-Eh3!Y4IdU-=pa}^}-qrd23MIqo05jLReSujGC=vDMNhI@`W@fgaet7x~`IM@p zY>;z`J(h-iBRwWn0@G1E)_zo=jc5xb1TPmg3_%|1cp`8ybq_mjzqJe{J{?O%UXf%j z#!l{M22hivjp|GO5`gCZK4vR{ZqQW9+usU44{`z0JM`&tnIgyLC-*=L=*ALx%{D8C zl3VDOS!@Al=HqP%t$f|2q4^PgoG^c|ELGP~rY2>bd7PY8ua$oRb>LGUe5$g@~$TTbo`NUQJW{2Ww3lv^CH^OU;A-@=1 zB2drxHL8cR^`So*9pbc22KonKuvd~3EJdx~@Cg*`ndS@&QO6fxCd(GuQ$?CA&cpf; zCnfAZo2Byr0fB@Jt`X_b6T%1-b0h@A5c^acn1@{AUC+x!y&jhzi3L%*&7OUAW*h7f|>fH6+@*T)0 zqu@%=f0f8UoEzEIp;8DkHTYbL)^SA4eaX-8F8tz2cmh5@5Bo&{tgVCQA!fUnE>$u) zH8NW~oeMD^EUKvckw-3e6fR9c9tEPFLFgP|0@i)CRHq!1yM4@G%Kf<8vK2w_k-eBV ztc6v3Xw3oo-Ez0P)hK|MoLzYs>xBak`HyCtWoM0Os8YfsurlNoQ~3`Zzr7?d8KHGY zL*|%1lh&p$4UEic!69+U4E+x-I}CPwAmI9B)^6?HGl2Hvg{e{1ng`<2@kB+=245kehoLpdB*rL>CbV7-q%Hh~O7CK2MmUWRpwfGXHK)Mw z=zeX-Y{wk_eG)Tv>nJG&RDo-!NK$iaUh7->Myn6)GL`wX=GEGZ=YlOYO?t?o(u;b; z!3=FM#n{h!GqN89j=rT{AL>G&jfP2oDKA~;xnG`6%QF8iiD`RTN}9-L*lCL7E%umZ zNMt;4(2^iz`j7U;M?goIW0smZAY2)w*sXge)Y7C!{mn6O&-5bNr8 zg?hFsEq1UUiC^dYIF3fpt*7X`D8LWYV^%OCmdRniR8?98h8JUxmz+W4s4TaOo7>-A zpm$1eKzzP=Ag!;pG@)@geHfjBRjGSg^S17;H99RgBsx{zm((KYa$fPQ7wV)*Gc2Y*=zVE@k*lYNIj;@Gv z!gI(!Hj(SVun-LG)eDh+X?6B8xs@_}IUzWOGDsUD%-&?0!~1CU85$GAIH3{#Fbb=6 zXW9s71csanQ1%RNa`4k-1&QYD(sdRqRSDW87$B6$GXM;EcVP$n(o{EFT_H#NR5N$$ zdEFa^S9w~A>1I!>WaMTGECswM?ZC@*6g||jYzu40Tc}RB0q@vit>mM~GpYH5LgGgo16_v*Atj-#- zCv()qM(hNV7;WYVT@*Su=F}5&sq6tdh1g?F{vhr=X5;Ig5}TJD0n<(u?2d&ej4*pA zEE}<-_S1=p2aC8c2R5}+3bZ-;v?k{=D}Q5&YF7^|(wG}L4K_W`8dXEFQhQZF>P=Pe zYr}}}K=hiYLmj9ODQurqGWS_q_fFfjjqKt6A2i@yRd}khnV*G%g$5yKe7iY8A(QsHn{F)V{!B`^uE*`DE~Tk7Egf$*yoH#b|2IW)2M|jg&QEC2(s6Ot}n*Zo%4i8*BMo67e9w-U)1sbbf+0mikeN z1b5;97b{0T@${kngJc;SkPibLHh76q#M!M{8bw zQyAps^d3%-Rp+Vvm~8UM*xNY$Lh(_Yv?mE?xt|Lq%BnN~df!Bl^oXL=u9UPAAm??p z6%9^N+6ZQRdvA=jp(eCzjN0i;-+AMj?#XrQ9L;T09I(U`W;ithFu5t1BbJ!-;!y!r zTvD#bVJ}+?y`orFVw0AKCA5ZNJ_6q2kUm0wG=)yXyVKNRDsNYNbB>p@NQJL1GC=^R z+|Z)mG!VxF(d!*|J#_Rp$sB&AylAN&_O8zVv38BG^(B90XsZasN<;KM>mBV`(ec-9 zJsz_8^QI%ctM|_tZ4fBb1M{ttw@iUcpKIPK=;cbyjk%~CItF*60++CYNpRv-_S>1d~G$&|a7SzD|<&g}>e*96*y3)IPN}u72(P z5rs-2*=^b>LgXX(wHY`BQ}i5O`0A!Z+e8PtWwA;yx1-l+t?C+Q&w42v>q0}f7j9T& zCMy$IYY4643H5v0-=kQt)7_&54<~LHIJmqMPMfMSf+H|17;8Es#!ng$J$V&Pn&UU?t++V__h4*ki4jwciW81e+>|enKmyw^l6PE2xGgDBg}x=T&ek�K^ z>uTPu9RK)^$0(sCcvpL35m@s!=^l|nVD`e`t>WuR&_*qy$fA_|PX#s(*jNo7|{-ve^a)7UNc+&yUK9^cSz9dd^7aZ*V`@OCM zdIJxMsJ5L?_ z4F=KYo^*a8o|=k^V2TyN7G4)fR9USzzoR&K*h$-Ru=6V7qLT$~k6ATh+cJnTL4^URnz+l|`NQ9Wm;s`j*rZ5O%@!U$F^?R^z zC%D7K?JA~$;JV&BWrV)A{#ITQ2uyk-ct`1k;U;9W44QkUUkLDk32SIyxRObz2YjfD zUlr~+2-%%Gg!6aQ1?vX3L{Gl&FdWuM>92arctJrFqT~qJotDD+*1*Wz(?)607T6>@ zdG^cSZsE#J^F6r(-QiYQXJNzTnhhB+yf{d}tKs1ud`&H6eA79Z1ki7n^`aRCW_dB4 z>CQhWStZk{Pm>lPe(3Ye6^wsBQJ2jU{w4vsKgXUCo4=A=SPOI6_lUGbFpwVdlH58W zvDJmqzU|2r9GdOK*54HaT1lmcWKJAR%*p~804?pqt#3Kq!Xu^cS~h1{4zRd$4uBm9 z6fhG|-?b>VbiSX6wZ42|&)5E*=3}(5cdY1bWy&3G;}%_%sjrD4BN>WrncQb-j8w8& zY^CDu@ncathKFopfU$PqMK5au-vGzgOhLcLJ)$nlw1d_K0JGNSx#YsOHT-QYNLxB2 z9T|d%ABY{uPd8XL+on4*%xpgl8-27lH(}ck7maIz^1Yf`PiqH?d*!wh^KF%YcW@uT{hMDpV8!JU0R~@3Ji$GS?T9|l)HX~0FELL1n8+6eMHhpfMpxX0px8@@{OWdj2do_6^ShNW+ zFei5R<(Ah1j;y*B-9xB0;|t>s2X(ty-oCOlb1xLuvnUj2KpbIlcdF1IyO-HiH=|OtuQ$SKk!$n%?U&Fg6rlP}h zEVMi8Op5i&NmgH9>d6xTmqk~sj)OSuckGH+)dl49vo6zpqkn$hq-B)x^djSbIdgoVGd{=qr8;_-o-}G9g(m==Sh4T~ zp>Di5(2B5v1%2y877%}p)v@*%FVe&cz<*G#?O*l|}ZcIRr9dz#SEK$O6xe zlT%ES5p=Y>dmJ)BKmc#AG4pmXpWz=sVgUf2{h41K0N(t>x#pMC|7B(SA{|2cpNDh} zn6Ftn09H;_ELi4O1Mo?_@n^k51S(K1ZvQ;2u>-}ZlAC)-W)XvN)sZ1fx;e;+E5c)} zx_YCCw5HycsRav0+hN`H%3B<}-u|XEZ4LEQW`WQAs$QC}jwke}OVyFwNr%grjft>S;(L zZn4jtTF$Wg6XN)Tx%?L!pdriYGL#qB-%O;MF?BW*sK-;9&5RO{)5|ah{hNR>BEA2u zgMFzc>Bvu`hhhM*WmdIq2yhL9dYGjga%cS-v9{_@acKYlzsiMbMtzSPlZ?P#;A~%D zm~M*4qkZ(NGrUM0eDNbUWET?s&g9LS?{{${L{~`HZP{)zpfU#M@R&*ib|$mF7- zzp44ztG1*v@ez)m&k)Nc?5PtqZ>OEvHf21COs(^wLVRxIFjMLGmllF&rC;?ZSG0_8 zqT549+AWRkdmZf>&U9UUU8RQOj=spmiO^cI4lu<;r$F)09xTwBcS$G)E@nsrC=5IVR%=@OPHB}e z258-xVjK|gxn?3f5k{F<0|?mKhq2Lf{i<#b>xV?@AVKSYk~(eeEVR9WvwBr8pe z$x|D5$-*i+gErSn5J85W457GqtZ?ONk;#?bBBeGq$KGzPKTBwI~J@s^MI)4#v6u! K`T|s(2m%1>d-dc1 literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-italic/LICENSE.txt b/assets/fonts/Roboto-italic/LICENSE.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/assets/fonts/Roboto-italic/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/assets/fonts/Roboto-italic/Roboto-italic.eot b/assets/fonts/Roboto-italic/Roboto-italic.eot new file mode 100644 index 0000000000000000000000000000000000000000..f2d020a8741fd850a1480bab13d0e11f6e914025 GIT binary patch literal 17534 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/fonts/Roboto-italic/Roboto-italic.ttf b/assets/fonts/Roboto-italic/Roboto-italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b0dd4a1e528f513a5261baa536e6a226200a2bb6 GIT binary patch literal 33404 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 RR;p6MN{V@9tNe#1{sOTsz$*X% literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-italic/Roboto-italic.woff b/assets/fonts/Roboto-italic/Roboto-italic.woff new file mode 100644 index 0000000000000000000000000000000000000000..dcfeb0083663b3deb1dae10ac8817ff3cc745c6f GIT binary patch literal 14716 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

G+XyUxF5!B>KCLZ{i|}g-j6g;S(`p2? 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

cP^4uIF~suA!3uz6kV67t z3aa9pjYfm-gG`8Kqz*?FYJ3{_C~%+Y2jPX1timSreC8oDs|`KB(MYR`YJK;j9{g*7 zf1*D8525Gy3d79fOA=N9B$s-0+D$^5!c4Td_d%w_UTC4$Iaj%fG*2K+z<7>;UrIuW zBask25S1a%a-P?el4n)pTzMo=`|p7w>Tuj$a_vCrxhs(KZV4*0UW~$bJ0N-Ly%($i zNEbO|9yB4}L5p)uqOS=^P_k(AUqi}Ue3zIDwZI7UyvCzvH5O@9%}B`BIR7q#JoUSf rI={)eHlW@HThOV$lLUD0!{Yw|Ed~(NflCkZ00000NkvXXu0mjf%BZ)F literal 0 HcmV?d00001 diff --git a/assets/icons/pm_dark_red_512.png b/assets/icons/pm_dark_red_512.png new file mode 100644 index 0000000000000000000000000000000000000000..77e05fc27e213a870022b1ae04e00ff51c7954ec GIT binary patch literal 29114 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

kxYJ;Umqb{P9Nn~s=S_nmtRKJT z6;&jj69TVFy99IYLv~O3Lx?5JJ?!?%O!vPtuDv= z8=s;=0YrVcwL`T&BKMusOS60F%QdB+XsX)>uY_Ul2o9q&W3yx*!m0$&#tK87W%D~g zmsosfv|_-XNi}U3QKI+hZ}RFrp?S3S1eNfVyNBhd2LM;K;?X$j5LXO~$tSdK+YNsDR`fy$ZlClLcJGSpmXa4G;&#_U_!F>kc*v}9RJrYwP7IuAmaa;F=D`W zKt~=nK{h>EQyAG8-i*`iBRj_Bn4R8yYg!m1yM-LS>|>uP{lYiI)n4q^3#5~Q!Pxe2 zomp1uY5YM=thX?Vpkv9>Rm=V2$czdFuA2V4!pRAs9u2z|8Mlk9qSct?^4G+65{=@K zaKRz&17=hPh<0!N8b+^`>@`^3q{2^#>sM>9UQ0ambM&*b$_9ZOkS3=rGj+kj9hJvY z91yv46I^oyx=@J~?e~&?Oqe{lQwITGJ}-K%zZZlyL0`U6*duCFYmtsNhh)asSac;& z)lQEV^sMfvNnBDRMx%ZLg-UcI>nnXOsK12QjhoX>8T zY7$ivhgw%t(631J+#k4D+NbORHL`WrQg(X z4hO#vubHorVc%6@l1%|s(sXH?heGXHv6(Os+EHL*U|kPIw(-fEr5~yHCEe^1{H&Zm zeM4vjiidNcf_QQ&5gcoi+Xky!-hU56@B4lVoZ6B<;i1rBQIZbEj#$`gU|o9zC}po~ z4oMwh3VGc_B-g}1wKF0^1MBl(eD`o|1o+mo+|i;B5e%^FoTE?drf%tju-I`0(kX1E z!2ew7e_Dt02fWQZz*(s=|s?7Gr_!r+hzu03@|M%Mm~xJtO!+el#^p&jnTE7ZRPLD(~JV&Te?~ z_mqof12_kr?bZJx9rpgmg~9aFPS$cGAgJ`i{OTGJM;WESmYb*p!J zA;G?Ey~&XYgxuq5SoWy&{%<`T6Iy5i_I;azHu6?7r&W?Jg9#`5&=7vonV_AH`haj|(bWBqD?~eTU zblHEn-t`Y~BsbyC6P0q_*F@1k@~bIu`L!c+4hFYc9~Blt#?3Oej&2$p2DMGD=Nn!f zHM<@+Al?V~XmrJb(@CA=N6ZRCFiRuk51it(`U=Ir1SGJ&p9|GSV&~({3Rpx))|LLk zY4IHKY`c!5`vaVhcwX-t6#gGjzXP0+KNbV`Xsn2Rl0!7BQ`m;Bpp2fh)pYS~WC?&; z-_QTU>g=P0o_yk!twQYYeC_f(8D0qg^?fhe;v0WGmf`T+1LY3tyS_u7Z-xVswE|g= z2NsaCme_vnNySGE<28gxAh-q@mZlcwX8n%gTe9!s9DhMshhlU@sq{sPR$%PL1zk9mw-EGbmfs!FM#oV0v;fLEKy!! z6?z&ZGR=rXZ0KvoQF0lmwxh-eT{<46S(fk9%!!~X+3Y(Cb;wbj3DnVLQUG7JGBXmO0MKI3OqzO?OGj z^F7|CDUrsl{!p~;L47<+b+`4$hRmNYnk$p3th156SJ1%|WL>>Uh!vCk^n zAGiKKUvVFsw79fzd;L8rW)E0eo4u?a%R%X_33Z2S_pSu+zA zvTJk4v``)v4$_QlHWT23_RPR`tS|aM8sQEsGa@B8&slA^>F=yz%DPnM&(U+ItXbis zo9jqlfY8F6`n9i|bd(=O@3xkU5@gT#*cXbU2-0u!zI3od*t@jcn z+?eF`)7_hA+^o)(>q3Kku@f)@>()>0UgeX&nZpKZ4fU!Peb;aSNrOF`2{QV9sRXUF z{7&EA7tFWX;5E5yS?3Q{ySqscfHpk*!7~|_fN9xSZ+5k38r4H*yb=RRk#!-T@Bi20 ziqK5YWnVd8sX;@eW_YoDzzd^8IacZjk*~ix;c=O#fX?7&Ve`2Ow6!*B%y(PB72>aN z^SI1EOT`^RG&Eb)_PodYVVGxu{#bK1@si8K;QYc0v%^O;I~cj_3ZM1YDBz7*@qv5g zwXv^5CwGE=;5kA=RHbQYeeZ$t(VLk7iPIhmEc&n)@%CGM$;L5H^MY9fM4^d&_Qp>>@88p9M7-Z5!n{mFvKvP*Pc=ZQ_WpXI3rAVMCxE4QwabIEE}P}r z-*!UkKuAa-@RNY8&YCuOQapIqzy9@XoiX>*&3x}Nv$3hC5~M|+SG4Nlo8)$5`uTm9LVNgm$gw5= z63iPX^HaT*666x%xDW&!B@6~h|8|WB*qw%h11+?M^QLD6$-#`}aE{NhD4-ONUso>& zsS(Z_nwnM!PdX{{w97WpqdFI)DRW)G>zj1k)TU8|fpZK*7ly);8aI0Jh9v8PR6Yvd zgAc#Le@TtDq_QAj1%xFntxL^5G4IVXktPS{OmV<@qVVo+Gk7)=QvK9CqW{8=6OS+!hIs-`&j;! z`MlI!^xwsq74rvKzV|XLMTWslUj1!gdFtK|&R9BVUr`F~E||>!n5BYUEY-bzNu2fj zD6RGVAe^PpSg~FJUFD-eZ!5>!aU*wUyqHvCVqIQIRrs9|yFiJ{VsS+rWq=5c4Y+cK z*MRI-P2D3yUmzx`zRd3w#RL~#qh}T4*i)b4GH&<+PgW)BhFD$ZJS4Y%YuKYF~v(PaQr z!R<+v1g)~a_EUm-6(Efjl?6({8K3ZqDV{Oho}*u>N=q8V>Z}tBsku@gA1+#nJB$y& zWNL9_|Mc8XRQcl+=&`40@`&Cu?kfPo_50~R1dVnhe+pd!IQQ~NRm(pZ+u(!}) zRcD)6&OXof$=lRb?QGz8`7ptm{_B%hXls0PU5Ft4!Yu0$lrorXQqE)Z9}Te&pl<1;+VGk3`2*Q+mq_M67wOS8_0fT%Q8F zsqSxh__J%WN~m+G%Ig^(^K+df7x+^5qf}ytF(Z`f@dF-2VBL(^pt>lqpUwM})ezIiX4Wy4$;B#M?jD{~^^={3Z+b%# zHBv1ukE?1{N=$?ppjX?%ydUo}eDhcVomqho!QAjAc-od^TS=>r3MHn_jPvfDJx4kI zm>Y!^du`F#KLL(3>yRAWhcKwC-YdYFF|z9r7dk1w3~_DVqU5iaLRgak`aXQ=TRYk|;_(zaMs@Cxq%d5ed(QChWk)7`de>G!QobitIrrR5#JPJabr?A-=$qg>p+i|&?S z(ahh!E35p6y&L9Q+k(?KD2uZ)zxL&QF9Zv6t1FwjMi0R}3{5`tBRH+%JdBm-ht7wO z`5K}gKb%|}Rc40jHLY)0w3PwS(n2z#>h1#Mt`a7eexk!azRCVqHx~T z_9ECiTh68z3jbmG9?9Wdo-m~~sAvyAmfSA!0BsmDOT%}sqEFD>P9oeebx2ydiZ-7C zOkf*{&+QqMdL0X-_RvKM^SGTB-$0F+T1ikc9r)J;bN4PA?vVF0%k4PU#_y!Ms98-VjYL5}wpz zTcB>iJ(YL>@_pI5nU|#v8Y)cK(~aukzfIn}dQ5WS$Q%?LH5eNPhPH%g6$ZHdw$JA8 zpf;V~$D`HEGd(8dG~lcyi*B2u+v&!{q82lYo&ti(HfU0#xsufVu++5;^jbNysUv^K z{6)D-pgqLO{F!!SwphvFSZW%;_iH4GrM_HZ|I@dVYRnJ46g^n=}W zj!WWrDBJdnI#8%aIek|T2}7w>@A%H!edb3wL%K8JR2}D0>+K1mZ7U_(VI0pMRR<|KJY{)7Er_ zPFE4zW{dq-Xs%}QwV~1DAx)To8v${}NjC2$1|xY5HlLJjR-GGvRPFtpcDYXSZ-^Xx6>twH4>)o!0j>1vZzo^R;Xn0Ce)yv8CD@-GdkCp6BZot*CZN)qo1PT>5MB+;}v1N1|dj{Y!C1)CFX# zNakLjeIrC>KfJgeb|j-j<;j%kWX?Ee{ik`0?OC<0FlG3w1)HlBy;B>YGEQ72YqITY z#P2JWm+81QTgjW$$p&)J%2ufCqYa^$8o&Rj+ZG8Sm~EZnB>RF#Tztf)jRp!~TZ{5G za?}+yElPH&?q=^+ehYUEJPI2B=n{4l#yj``lRHL7KJoJ;R&US{!K^E^zH`~h*^!N( z(vw-3)k?W-0-Hhgl_|SK*sxH8WxU_h1ELnQPMfW;FI*vpezvhfYx=_-a4I@N{iv&6 z6EV~_CyK2#2MxIj!t{|n&Vl6Obu=g!=hfB4`(B;5L}iJ!nV+bjNqx?Ue>NLw83s>9 zwr<|N3Ce=y^IWXOTLy`n&s95YCNat_o@g+M9v`WS&jfx72+#QSZS}I2;H6mCb4%Kjkw&NQ5;hYhgT^XPv*^?1#81 z7Ek%OHvxB|s74nNU?o`YUc7Z!-j+T49xav2f~Hb{iI&b#cza78H*T;ei=gqIocDwc zO)%W#)Zv3VJ+6uET4zl$k-KZ3z2NYAr}=?}DX1h!Je9GEwHje&ZlE7`L4U(Vf{T51L^q q&GN5F(4j_988_)Vftt$4Mbkr!&NPKhV|%2{cs*RbTxyA78UF|Pkp4&j literal 0 HcmV?d00001 diff --git a/assets/icons/pm_light_green_128.png b/assets/icons/pm_light_green_128.png new file mode 100644 index 0000000000000000000000000000000000000000..c499b717c4b41a2c11b30b6fd73975518f492684 GIT binary patch literal 11555 zcmcI~1yq#Z`tH!(Esc~QIm7_cEsaQtz%UHW0K?EVq;yD0w;(8~2vS2MAcBcerAZA-;ld_#loJN2Y48Wibk2O9LP65!#O?-i= z>kHTu#y%qem^<7u%Z&q2$-PQv7zXahkJM4ibbx6xK$~uM0pM#AfT861{d*{LOv`4_ zBX+c}d;pb|6|Ho@wh`bpK{uT+vJ+z=={(1_C+Mt{=SsPdkb~qH3t)+Dy)pn-uDkg@ zdU?|~z(-pS#zW@ZBZ)%K#~^+0{_+`*Nv5QbRb(AIiqw`mtc$n?G%sr($x(>ZUHLW> z1y;|~ zVevA1efT>=Pzk?4r2d?&)}3u}GjX+Ve&*L+S2lR5wp1BHto?J!ohLfU=fr-iT->{y z>IAq9Ls5@c2PgykO>1Y=pn;~aYW>dwv4**i`ydgAR7w5fgVb7)3|2~PnBav@y2?Ey z^Nb)~4U)|{4IOYxo5>CRAu(4vIE}|_ljyoztiEX2pwtMA87P++-@AEJ6Ou3QdtoLaAT$Db? z3$Y|+2+>$4?+L>KtAZ$)y1dyEaqkvJDzSz4u@ozz2_+amOd*FVVb$c6u?DinlX68J zc8M+$??-xTH*?4&Q$E)^P*eQ!oI%SsvDKQJPEnf`*hja_P#bBc4r8zA3$VuUdETs6 zpPaL-QJa7s&7=IxN};CITlexFSql3@No8#lxqAgEkIK0{Qfwa5uV9xXA4DHSJuHR4 z@-LSjkGZ1|R}%dSSVmCBTShe|J%+t$ze%)t*FoTjK28ys|83-fy@Dw^U#M8Jp(bzX zkC%)ua|+&#ez4!&Kztycjo#5FDd752x8c6QyZ&VT=qL5Ls4q=RLc8U<@HXvSEN(<( zfh8qSOz2ohTd)%NfsQ$&TodvV2#n0=N)b*M9LVs?=*^hS`26CE=ME1wPc9D`4|(c0 zo(coca*+x%y$!<+Tl?DA#&fS}4aFRV9Pd`ddqjBT9ce#)JZ|9edXs(p{!BBQ$=|THQ@0&F2`-2)7@u|4EaQUI8L(_QN+aB(4-*gd;wo12ISrD_ z_zWrx9Ce#2k4>K{&nnCs1(csN9KnwiH;u+D?91$o9Znr1MX2oz92iE{hu24!9VG2l z>{jh7hi58$%is$0bK`PQsI z7dQtsuc}Wwru_l>VKS9E)%#Y;gIh99QX$7I=dmP?2W-Qi2@LA+Qksffxx1-i#=H+M zzE*4Rdz-{@cT2TC;qvvsB+=NXk*7t|WfyV@<+c^#>Mk$m3|oM{RX=^s&Cji_$ge`r zy@=E>o`iCTHeyQ=^3(mKyF-Uf-%lt`mn-ZKErzfQ_KTU?B4*Ybg(kh5-O~0YmneD! zPN)1W0{MgP-iWkKcA?n2ic!{0AEC*Ck9bFoM>$>|y(Aelwqdf_u-O_k{p9;8?o;Qd zwT#Kk`FFZCd)0k!zgmvi?6-gE7oLM>K#QOW<7&JG;|vw16&Di>j?7zh6OSjvo!7;f z#3aO?I8{zPudl4ns86jo+E$YiZ@S;a=Q*h|q7ui*!T4CTyffE%*U`ex!tth0zL#|n z9Hm&)xizv{l&n%kH!F6YRa60~T+;6K(>B$n$W8-^O0{^@dr(UmNzNm_H()m2N7%!C zfITqHsTXe`uqOD^&Zp^(^&bK~+`W>U#v7%*#JzOf=C|j*w;x9SH2&@eyYKvZ{%bfS zA^vK4WA)YEgY~o0^cJJ|N%N{)H^ZebJwF&GSDZKb4(4`dC30nDpqHPQqyjYkm1G{v zBw*Q~M_ImK$f$~Qso#~EpA3u-gtv7DGG9txtzEpw)I|@$c! zs7-dnst{oX!DI^~bG*x>WU4$AdFf0Zpub$)udY*+UAU_JPT9SvRCzZpka>xwj%U^U zwTF|ulVi588Qe^3=GiIowDxo;Yy|I+f}eAf!-hkQL+JV9b5E`#E#mCvK)6?}Bi!xa z%{{|MHd+$0k6j#g#vr3(rBN>#9~P8!m2PXDu|<+3A=OOiHKaqK_>tn30kTI+`1fQs> z`L@j|o9#YZO)O@2w+XnJ>DMn;mF#O|#Sq3&Q+ZsGUMf1*nAS8mKr$w4on`fd2G92o z;OQ$_X+gY6DQ4Ur_xS{_Y89FnJ9M;VoiFVzq38Vr6*Gp;Pqi%go(4F^ea@7IWya<1Jh@TWw@BOY-lLkDEpOIK(A~aIsTM_yJsfe$O zr(t9U39Ttv#JpY^{}`M6^v$U0Zq7rH$KlW7(e%$@xe{Jt>)Wry=Ibfenm;p2@A_Q# zVI>g6(aiFTnZa8Sr(TouBo>}cGp^lN4@bvqjIyfYnmjK*d_GDRM~7v%+-vr!39!0& z_oU`M%!}O%F_$*8)0phH_Gz}G>FR`fwscm&Cx1WxvY@@QT-NEjY!|Wh;Fx=1q@;Hw zVb>$7$z0}R&_PMl`n@&#GvJb~sPvGuY95Ys+qu<2%uaf3`c+l*P03l+r>Zv=H|M1b zpEX+WT4r6AU9jgWy-P1D&#Q!j-W@9+OZ(@YwB9IwRGm%mlkxF)xL~+BGr1WQh!vo3 zoAP(Qrau31EjNckYO>Ry0k{EuKM@Pgq9}FD1|zhI<@U2X06D^I3P32Z?`E}~jsdWM zJ%)=k27E`iBmiij-NBl z=c;Dn4ge5P{Cc4QGP7v`0NhG@17n1-mZl`c#hD*$?P3Mx_jY!@MFRj2WW8O%5JxD2 z#R_U`50hrwZE9m8EzR}^ujK9ZuVDc;mOmf}M`wRBk&UEoj_QGPK#h>!?~MO=bk zNK8Z!D0ZJkSWrkpKoBG#1mqJGk`xq^6cuOr>yPc$8r<4OQeR2sFI%^F(rk7JgsY^0 zfR~pSzZa0-1#T-KBq4FjAuJ#)%y*06bN7KEz}|c?_q%^HC_&vJaC=vTy$g)x7bDop z#RDPDcI)Y%OK^7mO$&4XOQu`F1iZnn0z&+Pzn1g|&>Hd^=js7>`op+2L;&gpb%w$a z?zdQ>-`HD~w6uQ1|0S)n^KY~}LfP}yqrcYnFVXG>KCVy!eW<&O2OI)b_Pn)t_ixAC z5&F=7Nb?^|zlHyv>}uzNaB;VD`8Oi`{`$8Ru9AvyC>Y@aH*j%r`a8{# zZe8WKx3`wG1_7ahHeew>QLvQ=p9oY!j88%oBFbk46cG`ENQj7uTZ8_dujB&p_>};^ z=Kom>)-I4+j^D~jiVNF_Ld6B4d{8kV8$MyEh!r1L0wl^O4iOTCTHR6!iT-BOf!p7f zBiQMmS$}zDeai?G5`_STfmVDFsHotrSArlukhmb2Pte9%NK_OE6%iK`VPpA+T}2ls z7r2&-^{wW>e;!xAeJF;z*w{PWe(~4C@vmC>BWESm?A>o;>+@IjJ%YOZb>(Ew@<*CV zf+4>uNtzAvD+{33Y<~^g{|g!aGl&1A^|FKBlKvOD@h8mP#RlO8hC}6TZzcZ^vQOaO zx$h44{I8uCmk_arf<#34fL2yQw`vPR`9M~;Z=z6PL5LVc3@joF{(JHN()oYR)&Dc+ z|B@DB2Zq@~Z(Fkf+rM27#0BOFh5u7LT)}YgZAXT}-KE)V;4aQAU{_ZsdkFYf@B}+Kkd8x-*%s0dGp7@|7p|x zd+2s2|8@EM2yuJ!`w#+!-KIDE_K1<*m}Uh4&OwE`eCm;~(t z0|&qiIL7wGzy=VZ^?D%RM6z5HUlMs!doy|CeYF%^`+yu*O+c!=9e#%K3=I-2jn_In z-XWh|!1#jX0p2AQnMR(11sjrmPiO&TNj~+XCUdBap17LulwuFS7AYf77!V9N#*)GC z$EP(~YBY$Z3BEu|jU?&4FEc`~M(Uui0aP)Z-{ojq79(ZRC(%jJzD;SDa&5)|tWfGG zA5^cSRO;})h%1pR4O{F7Icc)n(a9v;$-ElO(|dbQP?h^;n;`&VtX2%o$|EITIvqed zT2R>vyMsEQ34I>BHEird4w;xMP>{m-Q4HQ#G~gwAI!2M>{()rI7K(7;!(5=73k3>y z;Tej?_pl@Y*FBgZc)aDPV~!oE12AH1xqj-sw2MM4%*}b5xMXMxpzo~Bc2{8*A)7|X z=o(9a4#dOQW<=RkD;noQJE}d^M|v$R57BAE*=Oz+`pSoY4GTnxCq6mJLrI~im$5w) zjo#AvP(5Qafu3B>D<(o~4eeCv*TcE76QTjvM5jc1?cCTW?PANdPoLj3V>U-W@EVu2 z%}kW?5kMf~aEO&ES0@rzpWHLY(N?Vw)g{A1^C!#!-|b}jib0>GaSUD%=;NKF8fmhM zq$O5~sJI*aovfvTG7AYz)L|6~(*mdhP5_-93}aYg@W#0)>cGccyN4aQC_R)tN@=+^ zo;(MY(Kkwq-@%Sl44y9litr#1-fO9%K%Ks2`90#d0P}&?3HkBbt^RjK%rrY5skp} z+iybd7B#fBww|(lvKDp~lI-v3YF)Q;B7P9E1d^8pX7BA6E`gZEUp#6F>XC@AV|i;) zL2rbU^Jw_)0%-X)F0g9hJxXMOnKj-dfi~p&q+IG~}soZgybu^rs zOS-XiQyS4BoWNM&lnxXprM6LOwHY6pvE5pp@h3$YH4FW0YeaG|J*g2c#$l4K zvD>iyJB5WH^0`GXwonz07-kF^lFRJt!X9PYaAbaU8lWg+snWGler-O`KPlRox9pU zyGd8|U0x(GNNeTeDZc>HzO8Z{2>>uVlYD~Mpc+# zsc)x5AIc{BF-kt=J5Q=BvJTgF-=*+!kwkUlk?P%dJ#mum+$<1NT=#NSGn`BIFt<6J z>?b=f;S826wI6pOpp23}>eg>-xnw8-y}28k%g*Zi*4NqC4;(=6eU%uxB{YQ@Y#9&6 zTEBZJN)Yc!4AQh=)QGlqGshYztCw@%Dca z?bbAq?j%rOz*%oDx85gE%kL|aj&S;vr@-k|+nmZ!4H(3tss!A~X7H4xQZoqNsl`>X~F@tLTlor)%Qa|qF^4GMk@mRn6`e)SPEpGofnGR>o9=;|9s zY;AGYZSVAc;Tt7N${bgj~L5VYzRKtd@C zD%qsNR-(>wMhNxYkprqfmyuVerjBE$wG%zmDNR0WOmE~4AULf3;e_xl`!4S)wU{qo z(|L!6Lhi0>WIKv?Psie#kbJX$yeFneUl;}(q^2YlW}r)ouL{f@??ksk)8}67qIDv& z%(#-IDRlw+`ic?2UrsRub;e=C#=Ax=f_`z2$<@VCu@g z`BA=(Dr`vGTJ}Y>=0s9Q7t10*eUY$k%GjoD==?OR6@eE}QcQGJ|0dOzGD^=csYLUTixVqz8hLau$g1@vuhbVbqiRt&`Ft>;||$ieDHpF5G4gteYiIZWtHGaE$P)?b8SP3*tFSjRF-Pd{Zazp zx~YleE=BdMu018G4R;m^)4kWzVU<5VXj3m#;mCi2c{hYcC?;a6bpq>lLnompY@qd? zUYdfH)dw{izsFuagFoq0S39~VN-ti7Or4I*->JkfoA3~DuZ>ZQNNZzdj~cb(ICWbM z9MX9D`Q;{N7iD<@DJ3&|UX2+GQf%-`)7ADNL~l64;?+;g@&xhul_{1FE6=RD$VO&k zL5l0%obR9G2y4*VVSR4-sThm>yvw^kFr45^UBx)LCa$H6^})P0z^|a(SbkxFc~2Di=M1P zshB9(5&%AC_a+PV+VlLUgD|0_!WJ9BIgLf zQ+tN|IL_Sn7AqY;KRw9sYkc0Dt2(DAUTK1-A}@}HIeRrcYyOF-iRE1p!-YkHpZ=1$ zU1164Ffv45aTk@u*_rTR2NF6ePZ4E4V8}<>u5s+>FC73}E2*??S)x%v2sKX3US&?A9L-cRu;vGn!6P{8X;3=y2d{z)A4~g%%=7BiE5Nuknzknl_xa z&FH}notDmqOjNl|#9Zo@vL9a^FhS_61D@Y{xg~f+^ZA?NQ?M`pTG|>m)`QU6nE0M6 zHNGBVg$2?)`hj)<2;)CQh z?32`A1ku?t7M*?p*b3rkFB-}({cl#Rj``^lkkl-0YrV|>%AW6U_BUC&_}+~ z?#=-tUW9w7NZl8A32_g`T@aO!EPeQMt*(}dFDze2h)F54{40ri=H(!)XYD^OT@<%l zPwgUEAJ?!28sC%5p#(PV4C$1mc@(rHv8a5Bu46e7e_3jC=v}!8j~WPu1mA#r-`PdL z&;>WXEg7AaV+B#X3BMl(`{GD%>RP&d{fX%Wr)Or{YVNK0`3IjZG%Vmj%#fp5_J`D} z;>wjMGv(3(BC}}yVFCuJ)+>_1{_fOA`~75c*j`fd6w)ituz3jXR?+5YNB;^>dGHuB zCYDxv4MWP!MQx0U zqsux~sLqS(YFbNQ-yBF?1|GyY6ci~LB$h>g*KETIkrg&%Xroodk@a7LE;4q4%4ZMTQvml9{LI-(6%kVs)ISIt|<9h@z;o z8r;krNPT*^j%cQH3kGOL(_W;CP3x|H3%Tc=GVo68Yz@M3B5q~;wxegYXAn}HT`5@T zFB9p&C=)zbt#?8(MHL<{8)rJuXhYO{sNv_ls>lCuep4M23tiA&V|7i{JMJJJ59frs zR>G1`I$VZ6`~{8*&a`)XpeW(-jp*Aw3zlv07uc5g3 znAH=OY`aH|_1HCT0uuR_IA3vYe|Msj#>%U_yb5MCCb7+hggRRvGKYugRkH0GykeCx z5J^XaiN7_`x<5$KPHo%U*W4knu=4r#DQ7PYV`3yZE}boOtf**c*(xANQZn26e` zR~4C~7?Sg3DJqUT?p2CZvEI>hIg>%`|K#oL+u)|M}__11lE4EAdYGAU5UxogyyQlHo-QO#k8;B`RZ~{A?C) z9;!66U5rzv&Gk8qZrNqY!3NZK?+R5q_7rcdNJ`D-xX4*Q#-SMG1zGRCmBo9@+>*@U zYJR!j;@Dr1)Ax$?j^>FFrdItB;1)?*ccV<%DTWtx1=@AC0pEw`DkaKyEK)8fx{W_W%Z$AEaYLYYZ5Gs_SEklW%*}yR6(S&(px8Y{ zYOOQ4yp$?w;=9?UM*?;G^czpsbl(s5x^bb?P?BHxVktoUp@-FOi$e34{);H>7N3Us7XPanc>pS2||!bNl&A zN1nQ0ksrBi<3-n`lueajCcdn-(KfH%6A)@6OO5g)IHf+X2{pVJVlCKHDJG_VcEnCv zaD>bJ-t9$>55s+<)_3|0uS~jhAVky0>XhF*grZ!kd!ty!gK>om}I+D>R8{ zc!(Y#Z`3(~T^ic<&BZqy$*ghnLsl0>Mrn@;!>p!N&cRe>$PqPPXS;ob6fH~JAwo>! zPc+P)+8z6dff&sz7y1aa$I-|s&va)3!|#`CIEH+*6F=g_H>pi$+e)v9d2(;QGS-$| zAL@ne|Gar-8Jf{Vv70HInvcd8rrx8civT2ihhMwE>knKjXL<3g(E}!zpYRd0F!wDf z16)|5W-*su6-~g1iM_1X4h?pwGG{&qz4s2gj<%tEyXly9Bf9$ZzCe5Pt}b_9;?j2~ zDQCmF^ktKYu$j+-)R%ac_iU){$)Epx44tFE#&1M#?F0Mypk?jjmTdNZrRf(K{47}2 z=}x&f9|1|}EK!3=&?P9Pf`mA`kGXy{1m+ZkjW#w3fZ0>HkXT`(A8y&9S&H7Yt+a+lt6|XodpiKw4SE z3R)Z;++7qH*P*WX%DYZ6Bxk#3_t2Wd9R|aKAxRj$A_9)&UuHEDxOh=tSA1!XRjuHNfat@hNEuIAn6~7+!|rmd8&>)XRBbS{9)Osu?6oi+o;sj59PgMAV0uzI96 z8@G(fnlVwk*4wor_zD=zOeu0c&Xend)j&wrL6`#4FJoCWm4+BOvS_i-HXs$1R&2Lu zHCRDt&2gD>5hgWtRPR(;QaAf2t5jQl4yn}K{&3aQ_zcXo6@ZC+ohwBEU(WJ>0kTia zX9@(rca*>o4FLgBb-YR4`}k(7R3|$4PjFXawvQ-Yv(HP9jcJ_FFHHB0Zba#=`{#wK zs%T)X*Njhswl-Y#&5zL1mgdi2%6=C*xMM&In_@21&eL0XRL0RSAn53Tg}QUW?1ZQv zUbueeT%5E`_~OZ_s&U}?Mqf1rmMQW^!5(asuemJuf^s?`6mpSxEOt4oI4Acdp(eJF zu(}!-r9u=7Be8!VPN=*xLcKZ<%)k$kd+27r^y=-fX3y;2d-R6Smhk~-b@znx!|7fn zW62~8?QoAom)j00ntg{G+$&}~+|{iJCwShAo|dlSDta|}|B{*w`2$zVfHy|JC6MZ% z!Tf8&j;W6Z(lIgbd*>->7vBDUHl6HDh4zq9g^}ZRN%FQTm=HhK2Ug6wd^k!K4yie86&q_C98D!U<#=Uv>y{Sz7Fiku*KQPKo;iFIET;9iYr7zooC>DxaIZ%21D?Z)Uwr(UOlyzD-(r|ZI<~iWbm14jY$|+z zXSpS!{F#}8^UrO9{nN@vi%Ty^>9zV>u25{nzQ>OdEX*GztLS~}6s?GB#vzIP^I0JD zlRII6K~3$FRF$f-?^{Xb-zF2483x}sBKSylfof0qec8F(zK!B znGo83fPaYYlQ*3FE)U{3$pg>As`87roCbN`WLdI4d@GTFykgo+0z~hF;J}1|n=3&Z zz8!p8WV2N+Rl8@6>6y)el2h;T0S%=q#es^sg3OnCn&J58RwTVGqW<003|wzz23x{M zBBlJvOCqvTJp9n98jlHKIqA-}Hs}0ShKd|ooSyP}{kDZF)L=38xoCw(n2Ktm8_rQL z&ft17rf%$0Qm+@tskia+>YU@n+0w{G+6TbgRp@j^s!NSNxUGOzSYGF46TihftTF}z zSsy%}?-zb!5*#8wanQMy>DiDZ`8IeL1#f@0H`tOrSqC)`Kc?I7(rIE^I3BdHdxu^E zj34|A4YE%7$_N!8lIwb|s5&hhY^3M98N=;AP;8ndaDEr9IfiFnV1!y~1GGQ^`t{Ep N>dM+ml?s+2{{tfpdOrXF literal 0 HcmV?d00001 diff --git a/assets/icons/pm_light_green_256.png b/assets/icons/pm_light_green_256.png new file mode 100644 index 0000000000000000000000000000000000000000..e78254bd3147808075dc987d5dd34785c209a844 GIT binary patch literal 18828 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 literal 0 HcmV?d00001 diff --git a/assets/icons/pm_light_green_512.ico b/assets/icons/pm_light_green_512.ico new file mode 100644 index 0000000000000000000000000000000000000000..6ccef48cd575a430f5acf19216d0ddb323032d17 GIT binary patch literal 113175 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%0D=LHXqm{%$q9-s900jb_ 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 literal 0 HcmV?d00001 diff --git a/assets/icons/pm_light_red_256.png b/assets/icons/pm_light_red_256.png new file mode 100644 index 0000000000000000000000000000000000000000..286e65fc8cb27010d27e3a44232c6671d3e69802 GIT binary patch literal 18969 zcmcG#1yo$kvOhY5ySux)Gq}5xKyY^$++~muTtWiDH9&wMfuIQt7F+|tHMqOWe{$rz z=e>3Ay=OgPty%27r@E@Ts(#hgyK7>#HI*>Y$j|@)0H%tvyeCZ6SX z007#uqnwQMATzOe$xCn2Y_bKDF zKX7XOhz#60%gZfZ)zK8K?xFP?;h;BurN2_Z?e#RB>Wv0TFev+vRe)aJ0A|n zKhN`Pml{QO?~>RoX?cX^a5tUk#ZI0Po-d!**;oXb?|)fL3102L$av zzzHAHEjqx=3C3A^8URh}lfQ-6_ks)%#cq%S7H|O_+Bt=Q*;D|<$G04;P%{LJZz6gO zz*$a!;^wAC2H?N|P=(%0$_MF&UroKswf_-zTgLXNP=vvVeT4|HK(g5!@>HmQUiV1E=-;tK@yg|-Q@J#nuo(=%CS;MP=JiBez=JG46zyc`y2{qt6Lb2Ze`;G-4>MFCfKnaw$q1~oXs&Z`t+UO0VCU}qP`g-{xI>Mw zz@dmx+rj%0g=ncHf=|`sPnUv1Ulb!x_qv~^L>|bSY^eGU&sH9>F1MUElr{;6Irh9q za2@M3#$WRrqhuPvTa7bixyiQ_8i6V8HY}uanza1=q#G1pVoX%r7%KZiY~Talep7Bt z%iU1>k_;Eertro}wyw-y`+*hr9fK-JLGzU~YvDV+3KpMtcBlb)R`6P)}{RXF_}H@_%e8gGJ`VvGG{W!K0LCKu@SN5vEj1ey`N*N z)c36rs5H^peYtDz_@&Blxr*eakTb6{ePxnQv`_w}=8G58`aV^A4AUIb3Kirpu*>uG zLQ3vpKle7;-q)wiAs)OxaPmdJ$GWF{(_OcLBBDZpXwO&{?G<;Pa;}qDxmCcVpH|MP zU#ah`-Bx{NWU8PYuU^N7?w9W`L?o9=Sx zvaMN7WBL`zp~&H@`S~`(;hmA%8M-oT6N47I( zvR1xo*PYb%H_Td0*qwGx4)QH~X4)3pCQmD~7fw@D8dcu^qHw0(C;j!}m$2)O5S5Uq zkg-ek{M*Lr#>~d|jRptGV!~}4ZJfR{iW7>7l#G-w1S`7pT#ud29n77d`(^rQhb?2} zio5qGwu;jfYe<)b?y`$3t*d`)_62DgY2xRkiwKH+_i6MY5;Fj;c&{}hv~YMkdIs?S za8sZE@Y&j}&C|5oG^3@lHPpw;FRg95MZ!)B21zq!HF+K{e^356q?p-s-Qzr4K3WpZlajQ382c?2q8=g=CK23tEV(ihniT5U(H%K_yg%Bf+PkX<{(N1 z<{qXmW*+$>o-8^l<}3V3S^x}$eFO6v6#D=qiQfa zq$aG2ysfMe=%R3)>8a$66h>nnT=7D5H%bOow2E_zwiLc9coml^94CfS|0b?y+cK;2 zaglLx&IvT}G|^ama|5~gax)q=fqIV5#k9w0$0)?e`*!`UFY~1aR?fFjPronDo*rkP zSzqeeX^2X_06QH`S^u0Wi!G&8E&SM1cA#-f7l-pA_D=Wpr)<5T-luiU^$fglo7|0n zEI*@H^Yz}x7hf*&Fpd+xl8*6*@D1^s+uqv0u=zfKHL#riNXtxD2C}CSmTZzt&dhl0 zCttxhlA4$2CJ^%L!kJI(Rb7==^mf7}f8b`}+0w22)4K;IL@io96Si8eo5EF{wVSTN zu4Mk-c2rp(Mb$w2lP(K$L-tbH_yYDbCVM8Qi@ySx&WXl?1Xlc>t`K`7rVu})j}Q@Y z9+(k)a~QMwwVu=4!R=w9+qhBl@l-849wVNJ(B~27LC&?#sP0>{b>_?$S1H}F;k(l_ z&y3CN^f30+cP1=89Gu*bUu3_ncWG%#xjr~r*xn5eRW81CHPtZZG!1c19Ltih&9e0x zZ2c8o<7(JvZt&%(=JMySd~RyJc2{_3xz6a(L`(8)p-iyR_gyswJB6T+=C+S^kt0LG zVY75|o*|VM&Pzhwt{)d9pK~tH4-Y#R$SXK12mQwV2#)+)`Q7XnxROl;+b(@Gj-HG- zKOZgPf8F*@4AK}ei~L^cG%z1AzkUDq+H{G!w&T-iyIatA&5&In1&heZdd53*H%bhN z*}(qa+DkP=9d44t5+;+<0>f@QuEQ<|E~+lAzBXN{zwBzZx;sQ4UWQCkP7M65D(qTE{NagyGzBzO#M{s0*>W z|7u*<>v^}!a)%JKzv{bgl9Z+yu@KD&kZ z-Z3BS`b2bh_$0jyg{U(S0|6|6{tNF_S3#%>LbCzz*GA_p89*B22_F#2-~YVTNlF1& zMVdl^7y{PdEYJaJzydA+9insspd>!t^O5=Nr)duIcYSi8(Jd6(<`1!i4?ZLz*#T3K z+_aJxEFu~$V~<_}R)>&2gg~Afu7DvA5=iz>$AJ&`SSegw)+g7GE9MI%{n5{Dh_Hee z)m{0O7XW~c|K|e)WaW?m04U!a^$ooZHPk`YU{@|n8?covm%pn!j2ZwCm-2VFw05@j zrm?cMcXX4WJ8tWsqj9v6pfeEE;L&iGvvqJ(4)nCu3)IxN4s^B_wV{)eq!ITA!5FyO zdRx-?ySlh}f&3-t{>B#syZ-Z-n~vshB;L*vbTWSg(im!J)5w86ZD|C#ggC8v1w?3s zMY(u|1bFy`IB58IctyE+M7VkRIeB=Yio$sCar5zU!YDYs0^GbU{W;yd=>N$<-qy?7)6w1A5$s0u zhohwx*vDIf4yNhfr{L=T7h5;4f0zkoFm8WKcWz!TotgF_>*nnRqvidJ7G@<4jlanMEn8RDzo@;u6?|bD{bO$bmfA}{z}=Qx*VYT{<7sWH z;0u#T|4+rdymf8=%QXMNa2WYtgWVm#-e4~W@PA{3zrOy<6z(87Pg_fGu%|v4?D9`9 zYyXoA4Ij*8G%OmH){bs}__F>{@*hKN@Mrwrv0wwXhVl5zav&>fAuDT35lc=Reo-M#0XqRJPAfY>0ZtKK z0e&G%AyHdf0lt57)ADqL!O_y?U%CF#$_B>KR#1dTNJ!X{(@IE`2gZlrhEtT+#)?zG zhKHZuiicmo%F2q4=D+01fnC6!8ekik&H4ZJxe9Dk>GkInHPto$uzLCTI^ zFy99J1ATh79{;#={~$?%&iYRj*xJzj%@V%;f(;^l|@p>U&xG{?E$u+uDc-TMNQ`B`hMsDIm%t%qeQcW65bN zY9lJhFC=VdX(jN_$^TpB|20)v(u$!-~=fC*F-O|$% zmXU2ey(H-DJi)FsmhSE@j@FicoX73!X7i6k_gA#gc!O#F3mgBf92;9t$A2;0ze@Q_ z1)Bd4<^8X2`p=;HKg8JoUvA?5v)23_KivO!X#M?O{_nO_{NtU!<#5oy1JQpPhp?m$ zf@PlnB^TN8+ggg+S_*L5@(I{+S_=wTb6Sd8S#erg3JY4=iilY8Tm22vf4G7F-{zt} zQS>)6{L2#j&quIz{mn$MFE6|`;JQ?t_T578u2v_3e6H+JKdm~UR(@a7dkBp8j5a7oa~Nr zly=E$grE1N_QG>~V&_k%*(yRqM|Pu`UfYYA-&e-$H5h`9L%w95FM8#j1zGKSXT`+C zHXe?t7MyNe=C&&2+y3*+52n}|jrkGX353NKF{lS7n z>)iOb;;HZ~lIsqs*htX;V?efjdo&Op(mH&tZ{O}T-r*zOXJ3-4XBbZuoD{b(My-_CKBCTK{cUCP+zF_2H@_7-<6&$yf?Gv8BUMi{3k>d z$`)3t;H|U)=#mpPI%or!_XoQoV<>@7tV&2lA*9fE!!yXxgW-Gye)+ zF;q&sl(vgD72aKmuvUr(Nd#_outt=zPu2qO1m{FNj!}L?Wdm?#Z)_pp4#zWD&nfXb z8KMM1hG0u9=uXLFL6i6h`tP|w3)kn)n#s1DAhJbx@yH5ddW;wf25V!7$Djp+l)HGR9XP%fkAuNFj`Bg1lo2HXwoq-S zTWY8GBmpdkrl92k_#UnXiBDBDlb1JWD0J!Eya1R+sxA zQfLNVFgB8Wl9p+=k0lC=5?v@x>dw~2R#wri5Q9*LXNT9^2{9Bg_~R44(YSJraaQ#} zK5<>OE$XFSOX4nS{OtwPI!kvRcp;1XgdPmzJo$hsLT$LVfJ6K)4t|!(CxmiuAti>Y zv#nh1f^*DwLxYt9m?XVdH?f|~LXeOA6q}T-piw6V@e1#Y(f6ZA)1m}nnW*5_6kAEa zy(?aP3TVe<_0)C1ahO6+rM`y$&DzDcZsiSlOu1TMS&5EH@F^b)LCpHqMst3DH(bL$ zf!u&QvZ+>A2Om=`8EeLJY?^UP78AN9mny3pHYGRF1_&#&T4!9wZy@YEJ3NUo*O>}ehJSBlxU;SV4#8|nTo-Q*@RMt%j;Lb1^V)ypq_9t zXd4j_q`}{a>jp1%)>FQ=L%g&2{{A+QnDc6r%5^WHy`S|U{A??TwvT7LOoz%aAtEv@ znEWkz?>gWLo(duQR;UEfP-vN1Q|P*xt=wBWQl>-HlOjigN|Eq{=~rfdfb$Z5MGm;9 zt%rfNTFKHsxza=noQzpM3RBwsm^_}@{*Fb-)AEa$M7drI@X@>=Nb-;RIbf`D_6tRCi`liSG6@CLqNMRl8(5Ab00TAkVWFNrozXQ9sp)xB2hq^thoFGBAK_- zt%P@=3vNB8yTrU1G5kpR!{eN+WWzXC0rlj;%GVU^uMDUUXoz$V5cE#(>My^5A&d z)^=htdidDA&*C4H50>*>Wrx0a9cWnS3%D=x3Y#?!0xO=cWy64uOOj#V-c=QqqgWRY zbyi1P1H`Q@u0&M~hHk^~9^nq>&c4RQXciNfZHI81B1-|RW?qaB<5g8tjPKY!p#j+= zw>F>Iu-vJqBi^PIP(n#c;)!0`6<~LzwPVJzh@_=5%q_q|FS;1jpUxvi3BnmJj8gOV zI857k>jH4JMEq%0QjOu3&=;xrV9F9k7%|Nvc;kj6jO~f+PPu0KP*W@%h7-I$n+9$p+LHM;?ww5m@8+j9;DX*((xq(@}}Togww$!0KbBfh0wHH z*N%~m`YGOCtaY6R4upo_fk2WF_`BQijRdL!Btx|Q*dUymP9_aW=mLF;?lUAdDW4b@ z-d8B1LtmQW<>Dc$Qda?Up=B=a zS{WJTn&Q1s2|g8aQt#RW%8U{u64ibx3=Op1V)D>d(A3LXZcy-h9oa!aDe@F6pB$IN zIs7N23bJ{?nvpPpXlR{GdwP^9w`Q`964jtz2-(p*m8#X2-G zF|wPv0HoKRuD~}r1Vk6DrCX}re^wl<LAsTHIoq(Bgjt&_v7j_3Ye zM^10K*&WbElX%7Bf#_aS9CR2?^gQZtx-gm02rg^kVJj+7hvT|(l}ApJ<1YE;z6J0r3B#8f3y#+zIak)mmV(VZ= zps$Tare63_za#UM!J`DWAMb?XzTRYZ5XK?GlKmv(eC7j?lEFRVsU>r*$q;Lvp@m|y zOs+(UU9OX$tx{-CF>5CX6y8Qv5A~m~F>mlHN>C%a(>NJmpl85=LCs@8WsC+m!kgxF zuCY?nQ3WifqUH~=f#ToBriN;Xp+T&2*tSUZKB zlid5xh8($;IY5z$(i8c1C}0}Ufza4%+R(v1xkLR)7?1H*Xw3x_+n?B^{fz}9m=yg5 zJvp+w5b#qP2_OEZ%=u;+OKzr`X;}M(;`Vc;&Kry6k2$!@EmXVaH%!s59b!Jg&&Hi> zJaRKWx9t5MU$NJC+VWPTaJ^nZ&_q0v5d`A)YT=*2ckxOdxfAq{lIkJ5qpgwb-IAPW z=Us~cda-}unW!=+`l=D`H1kn?ARDizdpKpQAh^&Fb&noEWO@ZzF3(@`Bi$!!uzhAp z4#JSD;Fd$>+R0Enef`2=h*~8%Lr88M3yePI%K9~<`YP;olLI7Q3 zGtb6^ajG8Y$~4>r!Ns$zs&}}NK&EOuyLx~*PA+jlcJRWR0m*4P4FdgMVVqg9JV`9$ zGie`UvYn=aMPE~(M|%1I+KTS{S03Izct83owk#AVa`&aE&#vgm`|3!K#iB5CmJ==s z=QAs@)|0?GT=O4bFY;K!fkLhDlHY`o=AD|wGI zw3Bc4IgHd7Ide?+ss(CLVv2`_i6gNLTk1G`TBa zBbCOgB9`vjNcyOlXjPxqA=kT=ea{24_p0D(Je8|I86tw%M=|7aWd95e7h1S2wga)J zK=?34*LJSRFy4#$7Nq8dwGg$pFUBPL&fYw!R@9|N52Vpjy%C~Jw0`k|ob#z#nK`ZG z*XLrZS)K~t`Bd8Ne635jHbmD}_E9J}AIhdr%u`t}bubE~KziWEN%JfQ-oRxY+IRbD z>o5CAwiBtyKB5Tkbu%ioeop*)+%dKn_K^0YX}Rb$*Y5)PROGZmTvf%2S2W^>BAd`k zj#=A57X)9cRGZT`)4)* z@QF>qGHedf3c}CnuZ0&2dh?EpjuQ+Myq({Ad*SceybZVR)4jf|(c|~jY8vwxstJ!5 zmwcAIkbS@~wFM5Qah$o@@80G+Zg8TGgUqIv4l)ix-{<*H5vIeUxJll{Kt8=Uy9> ztL9YsQaM~vvaOzg`?aR4XPpE3Yvh7Kg^chHRRZ~z-t~E0=q`&{iXvr1p;R*w`Kj4= zmdZ-&U5RU+kP8Q*W&dJu+I*VqOkXD)P*;91SJ@-)qoao@b=gW51IZ2!A=B|3?D1u4nH(xT@_PtYaZ zL=*Sr_2SUQ?_~S6#EvUhyzKsO1(tx=`Kt?9l)@7XIc&;*PMTS%qPU5!LvXk(g}x!*GR4TFf=t z>D0Mlf`5&ffM#Y_!s+icw+wF4Rsvfstc4N zv>;mOw@}}n-P-Qs9e^)e4rl(F#6j7^(VHhOuEys%{z>}i>jl(v0vlh|6ZqLa$!|YT zevl7~-@8m|X!NkL3pbocXC9;$m`f2ouL*6LdmwUpZ4s;wIG-95&VGiU{SJxt-XI*> z4=naoGFYfU6Bz&fBGTE4<iC+OA z0)+84GwCyAaNGa7-3A*zD5w{iOBoS!T5V;*%;>UOv8;E(Oxwg6=m_xQ%ZMP@YI}w}+De?jL!G+qUq^WJ4ZxsX>bj}wdLN(bo#n5A z`59(L>qQYFvoN_7t?XFc2uYIYh;Zl+EDL7~i^9(XC#+Z7&k`QjS2glKgEVp`h;~eD z&#r1@i@TQvo0$Yk7Un;A4ScNfzrkIfo^BuUP`Yo1ayB9CB9Mh+N4DNFm6w<@6?})E z#mcn|IpOl%Y|Ex?GbMa}SIFNI%Xwd9mv7=ngIc2dPOCf3;Mcc1Hg>9zck(Ok*-u+m z3`F!Wys8~FM>-ZOPKevP;b@(T%DI0Z2L~aq>V4Uf7}{4XFpZC&e@58Coo8!+=fw>w zmuDC5Zbbh+zwlnwFY4&>L>i>oQP4M;W8B?Y61k&pK6XwvIXjqR5PulRj7&jH|N;Y$^A$?fqZPl1hYhU5-r zyF|VBI|t^$UI_EL!yS80Z|4fk>ptGQ3?Lxujf`+)x)byN$QvEBe>yrt9U32BkkgSE zGIh{E7~r`GFh2$iPH^y2bM$Chb0>WvsvdoI99H$HoWNq3ISivf3^NmIU?->w-6VV| zerL!73R95gNG+%e3o|0-u=v%o&F4DhArP|nt>D;PSz<3<`JG?gKC%r-gk;A)rL-WA zNWmM|xPaNjL$a8`Fw@*;a-M}Ja3Vv#*V2W$J6B47cZr&ko`Ez ze)h1jDNjw+9z|k*kPO6#5B2iG(aK6<(hR{J$# zi=ReOIl@jlCzJ1taoE|@kk;;x`S)|YK3t!hkFRqn&*r^?zE(C`*3YVjy*E#Kc!gX` z`s4&4dS%u`cti$ScUXAO{+&1eDWfoVRF%K}VX`q$P>tFjB5#@t*6UJZGW}3qSsc19 zZB4t^hWlc7$U5XwG5eM6;k=;x8SuFND^p4BsIl7N%>w|Llv1p=4 zpL^v%pFcmpy(@A!#)P+hy#L7urm7#6$Oy=I7VkgmQc4{>ex7XEvf;Xj#KeMTAW8FI zVOj7m<~&=oq}4)KvqlC=bUeR>@!N@E99MTkvoCqs!Dju*zFE>s7x80 z)<++uXw7sqtC*D@&kJCoZnQOKe^>t7@S<(yGqn#D09;_#FvL@7Ip=`9>TD=O@9UaM z=zxAS2(0Qk2$t+$okMLH@*wQQ%(2B5m)>9R)tQo_o^)J){~nep3?JHzKlmP$( zMM&u?EYv*iL|#6ODByIcnaNsj9}TVrv0FyMpIp|s?gJ_&%%LxkEHGQd($Fr>2L+!} zegv0%fGO}Nt#3YbR~#89*yz78+8VWmVdBe9`bwIcmqeSd#E_2Q8lrbcqmU<{{p~l6 z*+h+M*-wH7WDusWGcP`f<`dYRB7z0f^M0eSA2c4#AnB z4-PNIuSj-nERt56l%3iuLD`8EkQ#9$`SW^X zqB!az5mzPx-4O3W2*YU^d_Mmr{;}OLZ zU)l>+;=#Wm%cs=JgJ5FO%)Co?c6n~}SOjt6iw$DtQOec98FZ_{)3yYQ4pG74+|_l= z@f;KNw1VxA@5snsUF>InkhD|BF^#SaPt%d=yi=-qrha%2T)^x843BcAmydPx6i|Qg zbnlqiK~r&auE3#K^-bJM;O4`wwWh_9tyHmXxQgpMlcU@tf`RSRvm!4Xo9op??GwtVCpViXe$xe&`EU+1J>X^wqgemUt(m%J}9IH&t zoTBP}2}Pe?A7dz2*Wyr|gFFe0edxTfR^q+sLMIV$%c)+b1 z`<_nYP3Eqscw6k^P}%PrJ>$J}#-?ZL-7Pz--psoN)EhRZ_@(?V<@xu8rajPMewqAk zY;C3GbBF34)iSLSZ_-j2j{Oh0Y2RC|Ubk=C(CPgG-Z<%hQq+V_0jm_imc*T335$r38$PPvwIm<&=-oux7S1|^D>a1TFzY-9rnctX%RW%PD>uc4wHHbe7 z`F{0<R~FbHJwCn3!4(K z1Fsz^(<<4}#WzMR@-w^kx7=j4oshniyyxJow}12Xqzv1gDl69y`Rxxb-#b8A;nV`X zOAPgF+5jF-Jn!>f<);qz46N0$^?MZU*pL%DR=2bL-2_S|m0v9z%Y7-|pBuC9e#oY{ zIa^<@e>XFt)-ajB6cIljI$19SyfmKefPOP-jGhFs5R?$#$@QN*b&3mQI3ri zA3*#33+q`i$m?d-seYn;7gzTYx(xv@A8AK|uUDE2XPZ?a+mu#>g=`uka0+9`Gz?N@ z5ETG(;qQ^J8|;JR$!R;&@vl4X2=ntUIE{$6xf*hP0BWZOSmIM>tt}4ZM{M{OeD>~` zp3*Hxg~mEG{rBRBFP?9h2k!1_H=f=UDLi8gu3Zbp>K!C5G#?+j7)n9us-7LCU!(%+*6J`<(ycfMd*Fg_?h{Czby1ta>Onxb_-u7 z9^N2$Nx;{DTQ>g8_gG>DtAN{`{e)(4U&&?Fc*pQWV_yumBMQ1Mb==dp%L`!IFZGp5 zqT2`jvXUxfHgAM#rqxo30*SE-FkSo9lYsmwKZh?##v^1X3ajBwLE%?eG_|CP*)&P>Lara;h~CFX6ujJsQFpl|aeUftec>z@ z0(?XA>g6|mIDBnUmncdxfL5{oC(6vTHADkbg~ug7@PpD}sMOdU@YcGG^MQcU)P%xG zuiRvYLA#0Q5Kg_wX$0CCRnEsl1<()pI=ICzE$(xsmC8Qy&?7r8#Fdu8wqDd2(dCK@(4#%Y!6%%l7jv-@=Pa(_aIANWC zj=YZ`dUYGs>@2XVXrGl%h959HcQ)@g9^r|L>>!sK_!IlMWcX`K^lZJV4D&je%Nl2@ zAR@iyMyMo81<;hLwQ0ejDERUGjyiGIjynSK{r*`lFj)!#)k}I20BEOJoh`?$gV^A}a7T&MqAZJ_*ht zLgY2DN~B+f1GdM*qd-a6XFy!?3G`;--m$9P9f0KMwq3v%iklYsl$b9;HwGmn@rP_k@r=f~5J)V&!{qWl#QAKN`JJ~3@em@5Hin%f=aM_sVh zd(P@tCV(8$y@Kr`!)L7@m5b&S&1?B}GlN2gPFe$?QoK@04Fx;5EePe*AKbk1HkXi^ z(^5tyc4R0z@?8(?LP}Va8Q+sMEg}PC#zikzd}2mywyxMCn?7{!qr0 zJs!~-17PSGe1kD@AS$f6sVa$iMCLkD>Q^iB=10Vh5O zE9+W?G^Lf%*94%Iy!e*j+cXvlM|$MtoPg!Ia+}$K73QU1ime|RSJGGc1nk>4kkHam zn1RJ!k6WX#a{Pe`{)^sAmm8G1BTr%+xvLK`uOaKWe#OuVjYL^I(Siww>NeLnZ;84= z)x9LitcF5Q7v#~-c02ka9uuV`GIG;g9dlp%c-~DGWbhEZOaJD=+xKTjiB{-kRt5}T zDPqm4yWI@ZWcyNBYB<#BhFzd}%W`TNa zNLEzRa~G;dk8shj<1P~qB$`H_otJh|AKntYceOiqmBRX5M8vyMDCndAQ@Vo*A%d&o@GDMn29Dp)#O8n`+8esO7MnM2YFFP*Yj%bgM^p63R zq9Vy!`&V6Hn>*PhSIN1=2ee2t1@Qc-Lmj7|?Egv3qwVUWCQh3hsRHf`*yau7;q|-lk ze^vC;EOY!^^QKwnikIDpOoz-xiFwkiC78}kPVh?d7LJVBbdLiN8d;K`QLrR^Z!P1? z(SlpatWo5%Ci{~NLK*Ayq?R~VRcJy1H7Yqj%BKG51sXS#L*vFSR+ssO%fRac_(XvD z9N1loa6?kmm9P7|(!0N6vz0j*MX=$<#&5V+G%gcql~=&<9QEQHtXPlY#gwc5C2*4A zXJLUy$tAIl5e5>L(=#>f)QP%ijkvufDsLv?3ju3~S-4F>BE@ONc7B=9snyS&=a=4k z;&?h!E)31qc!aGBpDACuysnrGq`QQYvwY3u#{{dV!==n!J<}XSsnd0hJvCJB^LPNl zluRSqMUx8zFVv#8Uez0(&RR1#sYg=)bxvyD-BJvq0PL^|fQg}J8q_6n$EuG_0xD&% zJo_jcKAX8_JyinQNm5?mKB&FkR6G<7y0`gMmFZbf1Mw?zLI2 z8N~^QJu)&xwo^pZ%ipM0sxvcHKj0q;Uf5v0}V_pa3{bkiIWP zQs$gIr(Rvak;7BYu1&z?Gapg@Su5qv$JQ<6>2{h$)(aeF0$ww-)$XxgL_Sc$8ua`X z4QCNYs<@dD7vNKUgY4<3KlkH8cDvL+y8M7B!S<<<_{adZAy;>IYJ&vYH?b_Vg%sZ( zt-ZO=GIMcB1Rl{&5@Ou7k=-buZ|Le!WTwfF?t*(M~9gkqSTb z-U21*QB9)l$R%Sd?(R~8JhaGuQc8uX&e$;xI4-a0mAM#*@F?^ z?axMV6$X%P1jNyH0h7>{okL>ZM!2VF5CytUcoUW*tTkX7?STZ-+B7_g;d?41^3|a% zon0<9)U)%&f?F`fWV3{^t=*&?F%T7o}DAm+TWFddO64t-EDux@h4)1Pab`cw72pVg9sc- z(%>@osm_b~Q)2;EY2%cdu)Uw!QC)|W0>4;d?$!5aZtP8$7#q3dK+#KyC4uwN)Ov-o2OqiO0Y$y0jSmVj4eqCC>OZ0iQ~U)K-MJ-#F$;E zVa;cJWv=ym0gPW+t7#>^HklWreIhh?R@K)856E@aD)4X0S&_HjGDe+nIE0;=!xmEm zn(-u+atnqcZ~q)D&^t@m&&Pg_ODFfjUK*t6Ci0Qj6c4r+u%imAtk5Ty1kRW`8rT0| zLP4oT3$dV6jkE)0t`ia=15TL862DK@Q{IkK-_Vj}`r?zl*m?~+{QvVdi%ltk=McqZ zEQCO-Rj4S437GHwkZkD*pwXE~V+nO_9noHb^(s@RQdxGsaeL`jk15C98Z!kV$HbGC zps-8}Os=)2%N04V6$NP54K<+nuovz!1%TWcss>{cXR!?x!_O5p@+ z-9&i3)^EAM_}t-1J;g9jV93RKTc||{k0pbB)dw#gIZ=O z4!3vc(7W)0;3YBuO*$8$3f4hutq357uYs$f>3o-qdJ>~ux0#;s!JQel7X2KRPQg#| z1@;cX3DDK(!P+yW#lj~Dl+IIFZIyE_K(OvO`9ppDeta7WReYpAE_9I&`{~w+ zt^+IM{Xd`|s1S3HFD(Z(IZyz~8Lxq|`Yw7jFN-cljLVbnT`#cP7Pnx(kK5+6KGq_6Q6|_RAaWJt_#jaDW z`3-ns02R7{o6>YK-Fit!@y3pvM8|I`wXqFy4KdF&BhcX?0%mO8jr8sTzC2L*{Jz|% z{wh=MFc@PPAZomH;f(h^Fje!TI(G+^Ei+wLAEQm_(}rxF7$Bvf-$475tJWq4hZ8be z(_C~bm_n|ivw78%lI%*C?S0nAuJ2F@CgOJy@Boc^QKXwhd$Hg;e)p=8*YJHwj5>O9 ztt0*{Cqd=ztaKE?5$pVlu;ysM$k#R$*Efkh{Ay9?JQS2j6*v^Z%K;_E3GfjGZ1pUh zJNZl&97N|7ssliq7V3}vPq2P%xO!~WR5E^thfE9+%Y+`eVbNsYNG7^(?z|Ngf~5qM z0Ed$`;c@XUT?h8j_iD8VJQV(ShU?!0=O_?g(wX+%3XLW9XJz(%b3YT1M3g% zjF>Qi%56v3&K3YU*HeM>(cHm^u6Y5?k@(CkKi5PKpnDdv!4X`H-ekqdUZ@btn)0DT z7(9MFspe*|%M4`s9ZJRals#k05xDd-`p)CE5 z8^y58eoEWgwfQ=Me**{8bE-3Q;ceh3bmq5Cy_iyLs=#3(fwpMh1&*e`^ZxQ+J{nXy zGoPC*xpM*wuDO;(O*O>xeuwOXdNvNt_eMp^GL)_P4F#6BD@IxHFfsyFW}N_h!Lu#7 zFbVh?>LIsg$(<8ec;k)4%gb#8gYAg}rywo69rL|ek&r?PKGy)#d>BxSvfyE41{&|R zABuF}PM&YrfH?%XA>W$}8h1MVzIL>2<4{LZC_}Ihnw2;!-y4RUXV8x4R6H&IEYS`i z82Y1s;BO*v605rWfH@B-`dHI4ZyqzppGDIrpAhfe+op}UD>g$4H)AQCNl;0&@NXd` zVl~kbpfC&q4n(nn{Rz)#trk>W*6G~%Txngnh#BL?1>pn$`T%2)BPiBsQ1!s0s6XN@ z&$ett72*`_$7{J9exqL-Sz@@qVNu3Hfo59%M)}a-`1!kE03CdF4ujCUOAbq71#+c1F8|FQDVXaPp|)%aLiw z3FH;LY+aN+4oo9nQxl{U$Wn)7J++x)8Ji(~%(RE2w&JCVKl91(5>?KQ+iH zY=?FSJ7#`A41Li1I(*iARDt$kJ~z=$;B=a2%rqT8?t;yb109KihdZD&?GV%zX_Kx< zqB7<=Xu(fIKcnoJ_M$Ch+U>n}&2{7>3Q8l|nok9uL?7TG=z~5y^R)$X5`)mO!q%hL zqH}g$4xoI<5UTgsgLNNIB~f?>uoP)KWc3{ep`X_v^fMX)3_w4>TGTRKnfbL1)kZX; w*vPWX{pJA+P#^w$@~oc-8_^CRBBBTL|LriR;f5p}DF6Tf07*qoM6N<$f{)?0-T(jq literal 0 HcmV?d00001 diff --git a/assets/icons/pm_light_red_512.ico b/assets/icons/pm_light_red_512.ico new file mode 100644 index 0000000000000000000000000000000000000000..8e0bc68aa2a15746f05e77e7cd17aa63e99516e4 GIT binary patch literal 113172 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%0DFIj#R-Um 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 z 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 TKFc`xk1i+7t<5S;-EaOstrv@^ literal 0 HcmV?d00001 diff --git a/assets/icons/pm_light_yellow_128.png b/assets/icons/pm_light_yellow_128.png new file mode 100644 index 0000000000000000000000000000000000000000..e0f89794e26c149fd27930f096b47c8dec577d12 GIT binary patch literal 11555 zcmc(F2UJtr+HL4n5a~z>9T7+%)X*UmkuK7tg^*A~?=^tZt02-vrAd`)0VGtBDuVPT z(m|S_bY48?T<>{r-24Ccl`&q%*vT$)&F@?DD|_uEvHH3iSIAk&0RX_2TbeLK+$ZMO zMoNr(?@)mE0RZH4j;gBqFh?(U7kB^wpc(tk5WEzc3*wz$6(?vTv}>lRs(#tutDBqewCuJf%}IVO_!(TXjfG4dF1Wg?U^ju zAkC zX#slqDVc=&fq-2x;$sScl@pFx=`#SE_8xXjVC04Qz!1B{449??wCd+R1x%y@Sc@JC z-N0HA+B8ew;lY~_2BpTW*nE)y%x|qc<9Rv%hCwcbWVaFwc=W5R=d8vO80c?ov zmis)_>Mp;GoL%z2? z;eCeDU!I(ag{$V~)AJae2;3J{&CTFolJDGKH|RBA_Y9loWcU!h$*|NZc-Sxg^i!BF zO8e|8BdSY>b@42B?c*2Lup$vqjNzQ34$GF5g_P!Gkky6XIg0F&J$;raXYZU^$4|Yq z6RMyUet{jnTNI?MgRys4`shM?&1+{gk)h_UFAax43C8(%dk|6I=~H{9`WbX$SZ!fk zgz$w9=Bix`#|)UP2E&EUzz3X_^r8Cd#Kh=>t+Rxr5<%&4Z0dV?Y%M)kD1Sm0VRMx=;`Z9L?)yY=4QX2TPJgZxQtoFlFs=tZz(N?FXtJqR`ZXkss3xzJ zGn6ywDu3+vPKiaTy%>MpW?qFfx`#UZnyQ~3vg!n;yt5NvR@LQ%^e`{6*2Y-ea^)%S z39%yxc-XAjn3lJ6yEYj=PEak{R;8xIU;pd|O*)SjR87}R>BiIaJ7oes=_sx1%fv-# z`*HiRS|y$@g3IJT$Fry;7R9}Qlv0%5ETtcnA0^&!+_iaKR58aF zj+9I@*1lQtt(dJi?`gxxTgRPs@3&O5aof7oPx(L9t$VHCT)Ve+c))NX5y;q*+-9>T zw#77;KpGYE)P@csDf&ZH7hDB-%gm8grj00uKw`2w)5RWx`?7+v-e-+x4P~ASvIsH= z<_ppYUVAhtSZ?H7CSGn~ux`9=?^s)5I#scQLds594?Pc6H%vyY9ZMZdosOJi#2FlnoLGm~ zKCX={IYAxO9abExKF*W}Sk-C7KSjQvFXbH;{518{N2@&k_Lppr^EbBJhTFQ^%&t<& zgLR8!0h7&!bD?uctLny#A57n*znM)vntER^>mvZofU4wKxIB4f-{^FMH}IY=%*LZJ&C@<~*~I&ymTWHE%xs%vx?npo5L&5)6}b(XC+T^o|hx4zUsaY(lytm&CQUOkZtj4^kI-SfzEq> zc}@66$lK8~0MhNMJ(XFBaBcFm?67?Nrtxj4kC$Is)8{wxeo}sV9`jpsU)sLM9GHIb za1}yV%uhT(BqyCOt*^Y;m0LS5dE8=>G;UR$?_vD)Q};L4@n!Ue@c!KPtW3V50`hF= zt89pNFihdDLNXBwKi1~eLRNL6TjP$x{CH?msAp?OD94%n`ReH_LVf%Q0xf(I5+|Z= zVq-!DO0g@wq-9q&uJ~TbzdmzKg@Wvg8SN)dak{t%676$j+q5fs(rmp{iBy%OA*`tD zeALcV{2l!FtFN>w4%2>v@UV zPGTs>SH?QQ6{`v#7iAac+&~LY3!RzhBg|3l(ct}IvhTDad>gzdUP)fjhl>w=`44rd za+^av{c4>(J@zYa7~etZ$SB@*bJ`w7jEt7V7PDzRE$S@U(mCdeyLvbF#PGq3oI62X z7mHUGA72Z%%UcS__A@t|s`K7Cs6EK1+(~F)9)g63^+BwW$M$#aT0T&Hn9Dfln{L$+g!xm~Rw4-3P_rRt)++d1)+@eK4n=U2~E(KY5Z&94zz zqLFct9&5P}Nx{ByCM;qixZ(sS0G1^kcTG%qg zIdLdk9+{2w>U}#FRgE@%Z*5Y$U41w*_5{Rn=N+2BS+y-{FcE$|M>5OJyk-5i8cHpw z$QpT$iX7;Z3Y*}X^b9Gtah{dzKo?CbT;?8r|MsnI`g)mAd9U9`Ke}!Iw-8tRX^~`0 ziKau}$J-a?!j;=Iv<<7?i9tF8R*@~`P9LTsrdCfM{s)K(bB#$(bz+n6+1sJRG%0-7 z+?E^70W~4Erw#XNUb*`5_<7G|%xu3&3tAnRZErgN$uL_o3koRMD>!@F)={SDa#6bD zy(#xYU}3oE{c!S*Pi&Kw!iTW^qNcSQtB%Kzul5r1gYp_rNaR~jZ1>~0AJ;xUua3Jc zI<6k5uC%^9DOni0-9pwf>$c=ZJXhska$0p#EgIJFL+yur@ROhKE>%Bh%q9mZ1Oz*s zvR)pWUG{?#K-XKRg3%WYC*LlV=CBxT9!5NX0HEie7xs5|Ne+&E=h~gbv*N9EVjuX z11AXn#>BMd#>#Z`_Ikk#k@+0;;2~h4k2o^~b6NibFXT*~*#2TC@a&W-MMMO#`}2I> zdYY*x>avLlx8Nmn*EI7204QjGZFqp}TqXd3w93)Q)Z0`?8;Wp4i@@#NY>^`VXm=bM z0FYDkcZVaKk={UCq`jl7Jl9TBD;LnwPM*s|LI3@fyBgwaR_0r09S9gzp$$p_dgh5NH2t^qr11G zn=9}aBiz=_$6KBY=joqQK)e5@b@lp7CY)d(f4DnHR0RBMN`CE4XfJOyUz|sO&Fx>Jy^I3fksw2)mz$3#0;%SU zv&j7q$Gp4^k^hwDKNyaK{~qk_;O6b-<>2;jMEHIAw-oMBRZk?`+s)I+&CTT>QP%$l z3s4Lv8Bjn6j&OAS#e3tIlYb3C!roEG#B%Bq|D(5QmD%-24@N@Sji} zTpHQIz2W~7ECGdp|0l4Uog*sXzlGW%peQ#_G#n?gBN}dx1i8D~|6xr>2YSoZ%Ny>B zK;DAMbKzVSadfnU!r@ZVVo0#GuqYA*7Zw+jL<&obNy!LHirFH>q!ChLV&YQ&7!Pwp z`20$MU*rF*1v@tcj^np-P#IAvl#DnEA}l2>jue)W5r+uF;SecdF}Redl#~qEPSW-_ zo1Uj5t{mYme`o#Wl^u=|A}WD^h(T$<+wD;eJ=|3yg{|{jT{kqruSwEov zwY2^`hW)9f`mgBx(Ziwts)+v6Jj6A1D6aGT?nVE5UE@#tZvVI4=U3kRG4a3MbpLS* zcPIa~`+Et2JNUf_LAv77+Y`6Mcr5s$4gg>ly#-S?^3VO27m&${Z0HXA+$Z!w8 zq)o~tz-4hCQz}Fj!K5acnkV3oS1n|sbPzaPEhn$D)&7ly6R+O^FlXoCwR4am9;}$XAavK zZ`#0rmCGC>gQdb+*sz0Te@b16f&4g!U`anxyL>oD_n)^Ad*hSYd}nWP`%H&Zfe}+TT;qBV#_*c{i2UVK7TVGbKrSGdZK8;Ux?0(VP=qK$=OB{b9DpL2 z2k1WOi5{cbCKkaT(;Fq=Hw4U4pK(CPTk(RL2`xy^Y`NGa4XQ@s!>#wa7n|Df7ginO zDQ8ER8;Ko=sz@@Tj#F+WSaaG>WV~d*S_}|UV%Tnj%VT*1KT^LYd*Jg5zXb~*C&&>H zoJx2W2T7?EllnQo-UBfcu22DeobOGoL`yPIF^*t9tUFCO?N;FY8*}uVM3FTj2d268g!w<_XASiY1Q{6L7 zrny8`lX%QGl|W55F#gbrj!>&a;I;I9Q`+sIDV;0ldItBT8-- z9UHnJPUlETs7#Kzt7n}Zv*_9U`--ws*nGY+EB+U zf~g<>97`R(^DLw#V~=5JP9bfVkQ0yf%Grk=ESW0;s90!sR`sF8(nv`^5+66lY*d5|I$ljSO^1KF_2{NRX8 z5oOQ?sXO!H`RCKcNL{|$n4xEtLYiAqjoqjr?#$x#!yQA2znpXS3a7$~(Pr3bX7RDV zP3ss_)lVpY^*wB~!Aq{5=7=YFs|1&n?)~Pl>v1umVhbKa0M{1>d?0)<$BTyJM{^DY zv@gzXzoybG~JsL_(NQ;yLL3Fu1Ljm5GhbubS&MnCwNVf3YR5d*-@ek^h?wd$LeC!2)~}{ z0{EYC+sQODza~7MMf7C9gJ5JvugxWjZ~nkbkiZbaALYc~90ok5?O&vR-jk8jrFiX# zCQkH#vvTiNyHA5LK{CPn=lTuS+G>pt${T!c0ytnBQ=nRh>o->454Nt)!wMh1o4Mpr z&t9X&>n|GH1(P38l7PqHuX(<_p2W}AUqV|yNyC4!f3w$OB)bVk#^p+<2abzjEde;= zXWps&qP=AEJ)X)&vDblKipAzeo6v=Dy7k#N{gY9Wk2T#|P)JOQM<~FnOihU39X(cB zmS8o#sWg{3Gb(t$=_I!w(!)5n>OSDI08Rfn9}T(Er?frvJ)9(Xu#$mBRW4dJBxxj; zt08AgM4$vuA?mVA57@9$fAnoFW)-!koXtsKfn9=-Dw}m);<@~g8Dw3uuX&ILs4oz* zY&{|`%ZEo3FKE?UzcD+LTZ{=19~$3sVss84A_5O)N$-m15-v!eeLuRS3@+)Hc%o|T z4BDPM6Sy{bsvkFL9Wq#x;c?wo>A3BOX4<{>LE@NaU9#Bsk!d}gHJ`p@)9SMpJlXdE z(B^3xVn46FpJdo&SRk`0P8QAAh&of+Bk^TfQ0m-1jJ$jwu;-^?f6lYb5GGjG>mxEf zxJXc;dPsO?Bem3bBRmfv)MYK!iCXHcQRTKht=G?QN3T?ED(M!!5AH9w@{sTK+a>5j zW3>RN=l-o67SSEU1W1ydXrtD&g5~+Mh8{tyO)sm%IE7qm0V#hDEhlD@n|P)XioQF1 zx$Qys%y`WGp*XHkgZ9zFaM`T)odvPzY23?mUg7tw3$lW@8X0b8DWLf)FR>V^mWx7W#fOfV?x6D6x)CH-g)huG5UfNNYRc$m zi+8Y8Yo^N*btR0;A1mvQgE^M2=^D)KIxE*iHpvb0>KCS60`r3NQ!g}bU6Ip<9?PAO z(}1~7KzvcdO7$L$EYnFgy$Q**u_QEK*t&%1RzlL=pQbs4sC(UvlfWt;-QIl5c-kd( z)vXdwtvPoz?51D|>eUfibApd>vfnrmn#3>vDV!z?U!9U0IANwr+l^;~qg z+tP7*Po1kq$&l1qqqUESeaB6u5nBqNn+Aji5Ans#PQ$5rY?gmqs2A>6*B%G6FH*A` z>`}I~Yr|0_v7IOzSg|dWu0$nef+nrHH#F-&ky&>}V<>7w(-*{_Lv?>AQ1(-Sm8feX z1yKQwe{yp8_+sw6GXuNPN9Bo(szr7c?U{FIIPGMYV9?rq$G~tr^`b7QHYK+%4>WT0 zqhO$>R@y2^wm79XqKZx zf#`&eZe>g3B9byI~a zedq8*o9?ELzMM9>37ZtV!WM}*PWH!0^b$hW=Cxg z0V{IxA*!|`jG~ZpLT&_cs)OvLHg(8#kZoxnE%$}lS>3J%RnUIo4XAyOTmSqGkjn@L!y(yn_uC9?$5T~wr8xrUZ*Ig89iHAQ? zryEHXm46HVzGu~0z<3JOp?nb1$1KF$sVJaMN2auQo+toaSrvZyAd9W==W-nwLI!RZ zIVG^Mg-vT-QBWI-N@L#kdPILIMaS0gDckiWpqmi8IZciD_PGK~%O>0V;!@a@Ut&9k z&s+#V10%lDOc0%=gA=OGZsZ=AYe3uLos$)*@o?Q2qqnk2ylH_0>rvuRQDJq-PeY{@ zG+n_=v;f8Cw}%t&4^}x*H6!e78rz8SEAD;=jf_<)lr@$nccBJSOdICiHsfM%g85mG zpW8yX&u>n=T>(Zl&Y5o)Q#u*@T!ZpmGWkgkOJ7-#cnuJn%Vjrg#ysfpIvbm?sG}$v z3n3`WX7)F!8ePyjd1aw0z7ZWlPe_^2LsvT)dLn66aVMfqvgfN~y5#|0#j8=31PFeK ztc59+?&$Kj8$&xJo6xX3AIFS@2F%X7y3nF8mj#bE6AKJ~oVS5lzzOQ=Cng^oe!24& z)h>^TP*{bxDva1@1J2SFeB#a@f8Dz75caB;b8498r9ObGhjy*dM-I-j2keMFjfIsM z-18A6Z)me$+m?=UDV`okG=uTpOYo^Il4RN;*^Oh59-8v-{m9f4`f}>>Ug}$r1ukHM zP~I#fO329Vo$^(9=JAcs?CK55i+TqYim_i5#_18(00qL}tUc#pN(BDG`+;SnV<*AE z>a=jV`^IC=H_hGa)=m_1HsulSn`k?AE3%nsMmfp^2iX^GkK#nCWFLM}kTUyW7he60 z?x49lsVx(5wC9~JyZDF}LpK&0!KY>Yo^;z`sS1udW!gHyOD!`!x5DL1F|Id(#dZZ+ zKzYiSD}6o1xzewnzozg|knNcac~FA^FZf!N6E!qR=|8!&8Asbi$vSoKdQlT_i_s@A z5#-H3+w4+ZpATk66$=?`Fn69FuTWYFRYedz=SBgZ?pgZ~xKD8cu8b<0t&=-lYuSuUzo~X{m z4ReenYYaGk75Yh}ZMSB2e2?S?eSh#nP1dkIcdVWJW@TZtY|MSyt=Q);H@S=m9Q3>^ z(4HP{7lm~!U7GAF@2F2Jhhk`A2T+BV28~rRDPwO;K8c_Em)LuE;AHYBlRgh5%awpJ zkfnSUt*0hK*Up~Sa}U-J_2~6D$Xi%~Z4?LZYp`x^n1*PADRzdq8rE}c zb68F&vPO%6KQGP=1{Y~#>fchg)ugke^1WdQIIZfAeP=Ij!z6mG+Ddb=0fazp=?w(n z?cV8}h&{3rY0UB?0=+z{;P(rwUprAhW2gi}D7~#B9jUG11VfN||lU9t7SnR|<56jzy-%{`SL2`+vk@~V*@%w!t&C2Kn= z%ADM53JHa8Egs|AQ;=*ynxTYM0v(k7o5S8DMNg7Ivtsnd?yQUN+CYX=|SXxBu+_?0S(X zy>m3&sHGFZP-ovdo{DuVovm7pzbYRFDyLSHHoG-+yCiMv9RU+9=8FGNgL7w=!I?>Y z@-$gy_Z{vcr9CoqT}5m!+04(JygHk0^^7X|#z;YQ82ezk(%ieD9AY!M zjo~z%Mh)LBMKszT+D3XZ(Ko)nIN!z>vXPmNo zB}7#17yjug>alstkNox|xqRGy1}c!9kvrzvvgO0VpXqVO)ql%J!?GRoL%Aj^(3CC= zg4VKTDQ;W_we)nYx0Xvgbsf6iJo&hjB5LYiJoFxaN%JkbOuSu7Obof}K;=B`7YXEW>Mb93`iSGn~9~JU1m#^@M^OXtt3O*Bh z6ckGA({~0LQ%O;aTD9;TwZBRE^Bk|^7bk@6; ztJB8#3YsyS*os~_FYSBG7Mf)7+K-m9>1T2&{eYKddFL==y|)YUQ(j?O*sJn`Lry-o z%WyfW;suxuFF=oTEY_s+qZJ~WCSquP9^Q9`l9MfXzSW57xS>UPqnmAF{LCYg)I7$i zaEb>*QZsQ`w_ZtkcqZpZDAaZ_THW``a7!(z2f+`>N$J`qp?L9P%#|;)dLFB}h$kzU zNzVbVea4(>%og%}z9WpW@AmGzpTO13719se%Wop|GVWn0821a?=N5zjLkrbhylZkH z=&V)3@Zc&8xArNl2vzZwI*qwW`S`&b@=G6oDuRpYRi@?LyLPXF33Fid9Wf)LzT(9) zc87bE5$iLSdJ}lP6xgqXBW35edlat{&pjxEHxv0tw-WRf#*)@_TW)Cr>s~0fM_cy; z{K}5W&&IzxDplQUwbYD<7wmda4tM}O$b}j(j>Ru0@ux_^>-D-H$+rn@B-*^|@{=uI zDW6PIs|atC6OC3a+^4N@qS5vhj|lp@r4`{gRnOrhNCQ(x(H;3k+@Ncz6joSzS9cF@ zBEhMjFr4}+AZfm$Uv+r0#Xm?)F2Sirgk}Hl2UXt?OGye4A z%)F4uRrLlM9V3u^sgOY=czrqv$nqrFY=dHr|Ndf{MAG5HtlFugQI(v)R-ESZCuIxy z64>z+phes@jeUBUP`N#9(aTG%;C4Ocmgcg(Gt&^p4{uFH2N+o~F&Sb0{352ecic_6 z(KdzUK*fU8pG{BCjF0#$>Qgq*G@|{DYe5ebAps6ktuVpWj3lCAu zd3!PuO<G=7qU~WNI9x98^G6$HTVZJNJzFk9Ks^UXB4wjf zZXKw7rAbmxF*a(fE7R;Y%a^pZHM45X0;XFfvqZGK0O0%?{Te#h7XJx_R%~OLGO5z^ z*>%xp!oKgqISQVmS~wHMGZ9|vv2++YV?Z?EGSX|Pt-#}kq;UvNT1T(er~RCp`ySSW zRTLhs%G{BGXICdN-adaK%b`_eys06Q$|qO|Z^%mG|l$4*p0QBk7Sx zpkj0Ul=n{QgFfPAd@Pb8ZBIY>jU3QAI+*1;x1c`IwSuM8D+9@!hHHKXxw@4$0+#k} z*Xupedi|qK`+#9OLl=F|9kQ3vm6^rfbh}DvdF<@Kl$4-6CB(;sSL$;d05exMZO}MG zRyw7@B#mz{B}5ae`#hwCgT~F$&QA>RCT>3Za%&x!qNZKeK^x*EAk>^c?6aKL@0G%yk zI)ksY)|H=k#=dbDTDp9w{E)wV&=Q<_{#|oSFYGYQ zpIARPsLEy;?IzIgM=Z;%y Ly09u0n~47b#HxYE literal 0 HcmV?d00001 diff --git a/assets/icons/pm_light_yellow_256.png b/assets/icons/pm_light_yellow_256.png new file mode 100644 index 0000000000000000000000000000000000000000..eda4336fca8f97931b27185a55f09751d98901a0 GIT binary patch literal 18756 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 Ef_A2BCIA2c literal 0 HcmV?d00001 diff --git a/assets/icons/pm_light_yellow_512.png b/assets/icons/pm_light_yellow_512.png new file mode 100644 index 0000000000000000000000000000000000000000..be87851a6a9c1592390cf86503a01f827a0dafc0 GIT binary patch literal 29002 zcmc$_2UJtt_Ak0qD3RWKks?Zm(0eZ;2nq;T2qhu(UZo_WC?KGsNa#h8Do7KlK}3*_ z(iMmz(xge1mbd-Rch3LZH_pBHKkj>PJjP(Kvohygzd6@zYwbn6rG+uwDehAM0MMD5 z7+eAXDEJ5ks42le+hHRI0B~x?U0>hQz&+5<%PAZHOyZwB4Yf>NVt=2zZYC;H3xCWX z!Sc+6$v{sGdMk+W$%CQO?-{E?*{}FX6&IuIUkMhFmQQs%Fw)f53GW&*bp~7?>5S_+ zjy2m@`$AB^iJ1wH+3E|=-O34V)gGYodut$XFI@upZp!wn7^NMp=ggeD371^d=PN?p zd-q#@w!2#v;GAlsfriD6z05P@8;5W=V5ch#S{yP`&nF(mK%*qEQ z9s<12A4#4g-hiW;RW6G_CZqtP`FZoFz|Si{IZY>*Jgx&qdU%lS_CDtK3-M#a0$Ndq zT?zn2={nyVU|2)``ss+=-7Cde>V&{$pJRxJ=D`?W2NvBGx6e?MbG}z2LS%Gfzx|H> zmCCHP`#sSDoaOxFmv|@rtQw~km<@B@C1UNO{{aUi>aaan7nLW+^;qD6i_jlqu zeG2&>V=OQiju<&Go#wpXjy2F^DH_Yy`Q} zJu`+Kdlyp==x_cIJG*|yl!lsjAl~LjFKcvs z``FFz@j4p_!JSAQTrW3p0$*eiuW&-%X;9$kMK7keMqZvR(vcvkoFwx&fzN0&n=hLW zfBT)t2}x{oO^F(~O&@5N8O`Qb39-~HkH3T~iq!e+WmhvnQLGI~j_;)S^;S3O6rfO* zN6#PR1V`F+@DKRKa=@wI!3_i{o0#W?e`7jizgOWG+WD*Yt#|LoFL_;%Lf zo22wN@hN|X?8WzH+ut+~5Vuf_ytm92ncv^0a57e5;eQt<^nhBp;GTidoo>Nr1`xRv zyYr8k(FPQ+vWrhgpH5~Ji{E;u_>F$^UYJF*s8%ZLBlBM-`X3+hnnyfnbv?_aZ*dyg z&GntP`rb8DACa=|C|6kcqh^!()a>tO)hW;fal^aLdaquDSstBZdMt8Y&CtSL=Uo2d z%cW<7A7jq*%u_y3{gv=5{``x8lE_lc(L`>&q~{4G$YPpeiDI@9%@N9F_hq_eVGo%d zo+N!_-sG@`yPgA7Dpo1g_JYKV)gr#4?EJbi0gXzcO+~VxwIpR#>%ugo8%dA6670RwzE!i%)xmTCl&6IQ6DtXFz3YR4Z#|7u^ zSlHN%S_hXei;PN+8kX|dFcjxpj(TjHuiWKe%z=e9U|tdk{)z0=pV}HueprI;}LI_) zh-{CNgImy-#W!-}Va@(&n`(0`?`8I0Se6sr_ zTC`Kd&}WC?$1Gu1`W*Tr`jh%Q`WFb}8MAekuQsZ>YbQ{{n9cT&J@PXF1au)fWz<9> zf0Vb(q3m#s*Hd7HYs_X$*?UomUrANzs@Kb@NA)l33H49vulzJoS8kGQk_s6&8a7Je z6Xml}EbYkgUiWmwI(m}3b-PaYImPQ2cB~BlC`>h~#c{yj%b<@JZf+Z_W zMNz#axIUO&{fgRb(ANg|8_6K|fN^+s=J_zsfs4 z@4YPbYi4a)HAh)K z4qwA9p|@b?q0&?y6z?f*;aarvbUoCibjx%hbU8d~PgWAu`9}BcDKp+R`+ImssDu4fP0;#w^>z;J`u#etL1dJCFVYeH z+s($c8C0n3Ln95H+^#DX(t-syr}m;r8l>EDCPp z*Osqsei;itv&BAmQ(-prWS8RItr3a}ntpb6sh>Ako3Vq=W8bnm+hqK&U8?_H`FztX zGm$otoh|s7@krnMmBXv%1{cD3wYT=An7)I}Uja|&Gt*)u9zMQyHds zM|;<&1*+Og23mb?wpc_hL3q!qY}O9NbA&hWcc+oNEq$73F+bclFh4pHPv|pqaEUYj09-F7!{o zZ93j#pMEhd6P~x3ca-1WQL62AQoJ6tqOp6HH2l14IAuLJzUhY6hnQc_n-T%1<^9?``wpv=4IH6!V38!W*tY#=f`z<_2_VPgsChWHd*#TdnC>p>Fk|zxaQRv(U zo+Ty*9E<(>bUllz#hM4=@S8|%3d1?UB3C&%7kzm-_ZxZgzEC*KFbwPfq+ZJOC>*(V z7ZP=(N$Ga79&vO?|3F&WWn=Gn*6}lEcO1Ei0&I8@ekS&T06@d?=NAHGWO0J+uf4mq zU67sm1vM96Z)qo2UuU#*n71ED4FDS2VSY|7p6DP!XSAEUkEYOiQ=5>WyQ`+q6-9Gd zb3c7F*4-o`0DU>a!rCRm(?!))NLx!#BTNkx;EfJ)5)AY9@(EN6(-itkt{V9KXIMr^ z@Gp`ePfa1+KNAYtnOh3#`v#x|6{VG=T;vo~1eH~#<&+d;kxG(+^0IQOGO{W%a!4sz zIW<`&HAQ8?zkh_l*#cZKYL^U*{+G=GUR3C z<)uIhslafbAg3@XpFrV%N-#hNx&*lU1-bkB2>y}i{|6>lmwyTO3l8x53$d$<4B89rjrIu& z1Zm~|H34W^bMt?Z|3@s|-v6Qw3^EJ>)%e>W|B*V-I@}K}a|s>j8yw(*HVgr06#l0+ zfkBth|7Fkr20KXp7qg$5egN7j$Tz^+*VpTxt8DpC+X%{oCKEhs?&RX`^T%4}{>=9` z3);Xb2(2k3FDoxAB_}H-uVO7HrzR(>CZ{OzXZ2%QwK>*A>Kp6cYTeaZ_-k7~qR>_X5B8d&4Ls=<1?|@eS~H0?q91 z?c|1*@$+%}3#7TZnyF72ug8V=E4SZdK|G2;({(pp!tFH?vk|73Jlml+f}nASyWp zDOF{plaw;r*~LW_DXWT6M*fMF|C;%qQglK60>PCI{5ux)oZS9?@^aVur+7JxlB>M3 zvx=0Qi>k7ef+|Ku${BH{2s$h^R|IYoFoPWTO`CsD&;eo4U;{FGJ_}}whM*IH@f#6^9 zqvquDCogCUx%}~Iw5!nH!|wlKb^lv-{F^@%iw1@MFFgGpvj_TOfx!wP8D=bS$VYbKb8Ma;F0ob z3dnzk>Hit{zfI$Ub@FjTgM~&$=>J?D7ddB)f}Fgnl&pe_tCWJPBIwgj%F0qsva+Dt z$)TN9oxw=^2hRVf&c7#;|IgKN@%0Ho2mG6R`8fqRf#nh%5U44H3Gnq6bn^4_a(8k1 zb#tmE@5M&QgkCe*~s$ z1qCqoJF9|ENJSU0q^QVal>T`2A8Ee-Pc!}>FZ#<2|JEP zfUz3@c7yOGgC78JwZhau*E%d~bviWtI=ZfFbG`4`-t8H^^_I|Q!sd*uUlYzVy>f!F z8B``vNCb4=q81NfxWXFb-}^GP8bYkR9-|4K=RZ&5vftGuTz-nQu3#@;vj{M$H%gHG}y}H>!DkC zin^Gq)O}^gzim)HtLw|TNP+a4k*+UOcL{8_S%8|L+Cs=p8x#df6=je5JyqEYxZvgL z26-s%zl^%C7q2r`xnzjV1NQ=QH%n#*;WsFYpr3FY9&HvBIZ(_9e@EY~x^M(hQh9Dj z@CMnAFP*lWvi#EFarL4_OEkwUJF~l|6l$}t{Q?Q`m{x;A1EKN$o2!%BhO^0I4BH%> zSiTbncmgZ1703!80hJh!zK-DJXS-I=h;zd&;N3jiw4g^Xb~j}+(6Tk`{!r9dgB(`Y zk!b>$pq-$mJD3})ZGJkL@u~CM{2gQbA?{pV$_xC$#(liKeio2U_oat8hRt&nuw${~ z5rw3+Jss}TIiBMu(UaQF`ped6sX$~`CdIL+S=I^21bxM5N>}L4xlH^n%NP1D@GtS0 zQ#l5QjD6+r5jesHo!Fz@Mf#XP)O0tl9GyeCW`u;lhP=+ZO)USI1=!hCBou9g!a}H& zD9?QD4@Zy3TX1WfI^t`f4+F9dHZ~2)Sj7NaHgD{4^y{fgb-?qEk!wkMTpG<9;8T1Y z9fCE$sQN)sXeF)*xC-?IOT`knS?s=~{InC%E-?J4eXckTZ(pJUsj8lYn=h@yn<1-T zls!izh}y$_L~R_;u>39iud0x$0-ig`-&U!bxw5-*S-BrR8I@Gv@e|G93j@XltDu2U zlwlo96-UO@EZyj^a_^^Uq9eWyGETyn*veV}wpNZdWR*Gmt9Iczp6#am}J;!pP+(?{3*9M=5RNTYl zebd)AM6_Ri^)+4U&$q5uNC3(2+MU6kc|%etkqwJM-SASuSOAhU0od#0>%_FM>$5!- zEWz^4@O{-Obrkzs1kP|0!S`96`N_OuS;i#M7dKmAkH7QktKBGQ2#qBDg`JB!=AqY~ z3|`QrUeiND+bDBrlHQAVAaL_eQbDg{c&n`ZBr)f1@_TbP!y6P;*dYTGLcWoMI9s3EY>D4y2Me5|v^2L|10pt0*Zk=(?=x=bM16svl zmMmCBB|YIX=0S!=Lyu@jIb}T)SOCv4yV}+mp{Yq<%wz;yg~6LMQUjb!Rty*`W+HyZ z@4>E8bUE7^rsPfH)(iV%F|nXLN>5%t&W+R74PAZCnlzCd`xF~?UT1AZEC8>(IG8bP z9ac}D+}U@dQcHN_I?BOM(iQ+$NLzoHoBV+o(*7#lo}2@3y@P=lQFb{aK)SNHC3?5W;J`r0<3(nAeI#4F5U!fF_xu{t1R%jy{3$+1jA{%0v5@*Zg(81>LDPPZMVr=B;cDPh% z!c>)56TiXp8bc^fI40E97?Ctv0wh|JbF+3{Ko(#<=z)!$^)JrIB}~o}Iq+8tV6To; zAjQcKu|s6mssIpCTF#Z>SM4>H!McAIXS7Oq>Ud@A2s4&@h#M?KLnP$HfcbjfQc@_h zthfq0QX*NJq)c?fkwK-o{GpaN?&h0EXBtyEWA5x|Y$%dGhoz#HR3K+_ZE%AHJ15B` zyS{SXlQ<=YS$4jAb;&Ha099zkP9+Q9J;IZL3nU3_Dyf-dS5A=yJnryVq#hh@2x%jr zS8|J8_sxg#c7a&{>nszIKy(t)L`emOZR{U z_ich#g-UT#4rw+lyhu6!aZ#r{CPzy;Zf@8ilPU&jHtA4&d*g~FnaM2O4ft3O-9TH9 zboG^+oFw-=m<9FN=|ku=h8SeOsi#iv=qtCnJlu-QHKcN`ToA=6WK&;@2Zcz~&*O8U zZiVK3X^exwGc<3;FS1UMAO<59Lg&C7CaA{9A_gphnTKOS6n2X!7C{tg#>Pf1J}veHJ@h~GTr9l6_z%L<=TjX4Bj+GE=YT*OjhgM-!|k^sj308}WQozG-a71BKeAL(3QB&n1)utQ}aGUHkpXeA&69UPC2 z;z}Q0bveVm?MU$>AK!ME*y35D9V#%f5_=;1nEF2aJuv4fzyg*zy(cqi`iAB2$4;W1 zR|CJ$zUK}``q6IWl2p$3BXsI*A<_58j{6U)8Ph2=5*$jk91e>aHa=M~Z(Lg4x}*T5 zJ4*Es=mmPAo`7e8g})~#25WNLRwO0=@}+UAE-MEMpt^?11NX`%NHFd3BzAl&uvmNw zMvSU+%ry#K*?L>hJ7!tiTqRk>UnQr;RKva;`R3Gl-oE7`Sl2feD8Uc7vx61aLvv%<2HuMmTJuS`K#B^F-RQzJ$e%Z5zwnC8Pl@>9M&y^ z-p4rm@us!C7zSdr9Xgu1rsoIscs9NKj0!a=KSMtkuBDe18nkCJl`RC3B=+k@L14Kz z7)zKNaDK`Yifc(d>8-buN9<3ghD&12amTppxH|vrq-u469mX(4wFd=(&>`+zcAaSZv67 z(rZEV!D2{sVeenkp3w8+G0R>LKvE26>Dpcn7?V!d7cI!#Blo`Fu+|;y@r9Ra`f>VE zSDjY#M|EyrBvlk+)97xo;M#fUBdIF1?KdwN(h*MXH_8_|*bM%vQO{sgVHJhhYP<$~ zb%T|?a9IHD05wUYcFna@4z~J*Op);;MoEd+Y(ylecEhgrT2+9Tijv7|$lbyz9wjjG zQ~JO+^uLU}FyW_^)22)Eorm% zdiiHuWj;Q}2P{$*)xr)AUj+E1>N&3y$pz9cpDe(2LUdpQ^22!RiKw&cZHyOpu7u~QKkq>hIjz+~$csAS ziw>kM;%p&I&Ww0~jUlRALF9V}=dk4Q$BQ~b)`}3Uv9aC^8=pz126ZH?qQ&M{yxZ7a zWoRyCNHYC1=QV?yNbxIDRhnuz!QA%Pm>hP*i2*`7dE9I2H8w7zE(HnNm+3(_n40Ib&9=a{pr$qy z?CESHKH*q~6?RzKUp=0*Ej|t_d3vp<74FB_lVNtOPYcG59I26{(pQQ#w>I698pc1K zSilb&QayYkF9z@w6uLd3qAV(tFvlg>@RtdXI2d0mdNVc+@J=c>wY0x~@F9amX|QR*d~bTLVp}7vvhinxVx(%xaTs5#M@Rz(-o?g997D{1-`r4^C|bagb#E4wE2bG9o4+@r`gjU$ z3Y<`d`35nHzG7@~;)Q7NfgPlxo7RTAHSOwil3kCpIw^5jw^olU4H(l`M?FoXe+Ivy zyDBSlpZ;B+gU`*N^Qc&oN)HOsxY;bC_5j1G3E{jmb+`t)EJA~_naaCBp(enAg>CcK zP0Ut~q0|q7`}EJO3>~5SBGE#x-&SB-WK!qh5nhg;hW48Uj8GV zO^pqu1jYTzl#r3=Q|Y&FTu4ki$Oe0C0U4@E;1XMb?U4hv`WCw zIf#*Bo7IoH^rG0zP^7Gd1_y=l*S1&X~(}W%st{o!a&twmkVG^ z=F3Px;B71!rFpmLeWLunK*HL=-uE5P<6}Z1Mz!8O$_`_TWRnt|0sF@G`>jwID!K}1Ed+exY2 z%XyXoG}BFJgKk7fT*HxB`MQB-mL-5J zT~xSE6xVClEb-+a_eZ+7xeWYLNlcNEosT%#PZ-4Hmxc zOGbHpC*If^$D01g>UyB1dxi<@qB{GGk+(yQsU9`#71;K^N#w@uL3ZgIL#!fsE1o}h zRq&%MeM1(P>pG$=qiJE{!ym%a=e@Zz@R|I4CU3{U4au8C6Cf@xo7@3?d7V`o9?i)2 z?vfGA5tzA+5lfKWAD7bUESlj1XQG&nONrQ95JA;7%W#8NJE6^|NsWg75`L%L>8c8c zNHGBSQ6g44BYa9zg}z4ofN5!Z&XF;R&JTk_}}1W}eGtm;@}5}h!1a(NivXxl>gR`Giet)+JT z71)em*n+4K!5wBph~lgW>0i9}8Q>v(n-PWq0!zXAN1PtR*W%R4YwQJDkQqiS``k4W z2Z+XlcO$=9I_(Nn7QT!GGCe7kKcrEElwLUn@BIDy8`k3@C)jF z{W`(tmlnh7+1H&vpf{n)hE%tT-nJ!_9`z%VMJ0BcPTZ_iKhBbpj4>sI+cPZViS#0J zp%C@dVgH|!ZortHdXKm1tYA{RKrPJ)(4)Sp4S$l`N-0{P-34|a2}Om^fi|tDwGuuW^9d zQP$zEArq-&iY-W}ZIP6Exhc3ZL=-@OOddP%g|)^%l156zjSg)tJ}HEmc9&tE4;$-Ve3iJ&@l0-ChANU$)E&)pbT>bWj-x<0jsK`WVcs-I=mR?s1#fmI%uNm^ z%ZnCYJ9s@&Q_TEruHC=3kdM4Wsg0Irj>;VrN;2r}<3>UoSSQ5P9^7uewF}eu*hAIt zOo_Upu`jc_@(!$CKeVVWzie;gTUGqc;i0$;wnP&@pt)T<&?cV5#`Jn?c7zUwhyB!Y z$VZjAM!M2X$0G3H6metf;aMgu!Bz}B_iuW=FPB)(Qt{f@ ztvVHV8NW%qMY01=T1)wNrARf{gucEW;U|Po*5yeUzpF)~2qF_Ukno`rUl!}Ks576g zT%@sZe78_M`bGXXE%d4b))t*o*m&*?AC^6@(1u~6#~kNkh0jd7x`U(GJ0*T6($SBX z3gT!G>&ivhGfa#v_J0`qJM+nB6OvIaWI!RaS<3IC1b_JxByjU-%{sNngotth|)YpPv*)kE?km5?}R;xOohneQ+7&@F8+Exh_Z z!KOd5_P%|Z=7oNTRQ4SztPRvpR$Rh!1Mt)JqxCHOp-gi&B%2&kOSx~RK$mW6EQvIm z=_mRY#+r2J7f;$>c6bQfKRuVzNwKBqobrs0LJKN+@zr?$)G&G8m`HeB>Gg_0H*m!& zR^~GDbzMf(16vW`D&*>Gl7A0L>06V13s)GdGv}aEdHW5ZxP9)IWE`RHy=L*?U3bj( z21ei+8$zeImxbM$L#>8e)m)Ry#`$MKL(|D;tIN4tLpKr%PPF^VcTO1AVWsNKEmxGv zC*k00rP(tAg_1rL(E_|3hi7+Rk`FS-cZjNg&Xyxi64lDMy>OVDJY!zaD($NCsVP?M zvw|U{Y1l-{x3$OK`c20%aYe_yBQ?Z6hXr2J^F={sSz47U0u}ZQWKqYMX(|JeC*lLV zq;5XJ2JLCzcc#zsE?(FX7$&W|#~@ew^}}RzrXdPeEO`VtO7Tl2SM5wR{j1Ei8Hksj z|N4nB5ToxFRmdYous>)`*@J@r65uty$f2AH{EX)z178qFr#B8fA;wl;g2`4yJ|g*B z;0Z=amfRT(Sx)xYrpW|1LNDSgrBc_KZ;nT3RPlCR0ODW_u-vzk&7h@2&#slbNvy^% zx6(QkB8U(7W}3f)20ybpqEKbtTdbjau1iQ?LhJ2GO%<<}+JOn+MJ zc6b~+^dqIfJEQFB{xM|Z>7N7ei4({*0X_rhE~PE+PY%ULzy-<>Ptr=y4& z8s5yC{0iX#S6is6L$IyL>qnv3)sgp29r(I)V|+o!;UVmPj;|+&uai5($u2hE${fWdsMEn2qIsghW zLJKPd`(_HF%()v*wYJ;hL`!t9P|eu`TTz4vbGDRJel6 z6ly%)Foy}L-9u5)ZL?6N0=z>yf@paPIxN#JrBZ>44LL4PIA&I4Ry466Ux=r}?-g>f zAj^_Vh`9x^)b*-WJvv(cc?hB_>Nu|B++DMK_AvstJY-niS7SIggh~0lm2c=bq!o^} zdHC$31U#;xF{A!YK|uSNf_~gFHx^o@F55bcJ&#iI^B=0)(}}gS8apx0pZXFW-DYdx z{ZOpBLF!lJ$eG_QVe1L@Pre|&fXzT&hy%F8Hi2cwo&PRQ+QhBU>#nE@pMC#6jlT9X zx#|0OzNdm8tw;QuZOIRj)~cIu1t|<>UioRmCx?;=ZqCoMVss*qbZW0pK`Nc2cTQa} zpe6Fa)Gud+JQW_wj=9lvayL(r+`4NlsUvfn#d6O8Dbc3==X4QF{FGHx9)buWck$*0 z8%@J6lOOem9!D^(o!o^nJn~Ka4jcYy9fSA?9l5t?cdhqr`Kclx)DYxjt+I*9Rd@hL zd2au>VKZJRBIw&&xI-5DdE{$s+7n)EcZt8=5JA-0GmBad{-{5 zo_hAVFv`AVSq8$XSNGyVF64yEZ`z8Ovv~Ju#MfM<+NB>F?CZqAkJO~>v#K87BX({t zn^0QGcJ!TE;3CNk>%tuf(P`_4W`L4j=vA9wQCVS9V`jcNui?az@~NoYweF<|szN>O ze#sr`$LCBOU8cs-U|@bhgoa za4T`9K{*C3%>LXfVv$1do@!2tF6!wkfAA8PlR__KnJbnHavw|^+BshYl;PJxnH@qt zxTv+*cMLbi@UxN6K#lY8jW!>Q#7f!x06*6A*uHWNei1Up_1t8-R>JX0FZ|>fAC9JJTnef9M5fWXoEwTIlOBc-V zLzbmkOEj8xIrU>JUH*yorWPE>@9U+0l(dKSGKb2fwTL!sucFjD_wt!|pB6^B}X z6-z0%%9##Nf_*foiMacLt8eOVK00ZuBm`kqQymUbq%JLRDtaq^V4LdN8nm+F@C%V zoEYw2uYV6Ye0co6Z7mL{1hkDhllscR+ZT;RsJWbz>eukRu8S}4wNYFPlnS~L=tHcd zDrDI=ryrlB;nVN;Tm?J9-}|Z=5c!&4zj->bFVpVSEq*O(eF4wot1!G`B_@X;Vz7HL zJ#IF4A$tWcW9%L1v%3a8m)q!$XHTfVWPDQWmyBV6fp18eT+JgES3X^aR%uijjSY(| zLw>1F)z|XHPcCV3>JR;BD|b7$OL69bqr3tw-4WBB+&)R|t$Ryr8Mi*r%0le8%sH%w zcfQALdkRhE6Miv0aVnP98>vi1}Ks$~nE0NKf>ecFM)sMQYx|I}Geo0u56Z?ABwMuWkb=!JQE&vrPxK*02>e_M*nX$OuHBY6CfHaRJRfJ3q$!^|mPxY!-d~z#0Yc%| z0{OP}P3w-<8Le5D;_g@0P*e&17SZd##XKN;r4XLymv%)N=Pj33+N(kA?k?Bswy2Ua zu0GtIB;w*(>g) zlC7!~higN@!!?>Ai{It7g_jxiKST?Ex7-eFm77~hx1VA!aOt;EcylNKsx;Oull<(9 z`N_%#PoeHP8+|-Eum7RFJr~=H?g;j0Eg?w~CWr6t5a#D*7J*xm7hj`K)(|@CjlvZL z%`eEhZt{0i*Ki8ZhIlUt;jzyaZkjamwCE$73YZVZtP!q~xEy?PjcMm9B<%w5zVK&W z!mk-7Em}Uv1snA#4*$@&@kO9sIJb#>1>g3-ry7mRd?vIfYfB#>sjuh;aJH2ioTV>hpRb2IWwclDSo7 z_P^oMjHXgMc^dqAHlMij>@Z5H<{N?Gh-T58^_oJ(2 zL5>Q$^A(#I*=|`F)(L<3Z@z^UgQAH@GpF>%?^{m)9M*L+1{z2l*qIqZI_v zq|_cpVydnP8c!V^=JQBE0{TNG_x|u6)UCKa=SzAl5n{)>7Er z8|w!A()#p9vXREZ-!0Vl`1of80Qc?SVq8j_+T_XX-MnYgz1)--Ol+`N6_uJ=dgwYKr&$_OoMA{b zQ*{i4%BF1>#p)nPlBAOR{4tzrPy!nAEFTM->!Yo7!i*Kq7W{gfD@vwYoHoaaJ_SmD zi2k|=oGH-LId=xw=?M>%k2s273{M*+x3u1VIL}Xstikj{1^^}F&K{xrRBFozqI^^o z#7f|m3i!ii_MTP%Vp zQIwN>iM;yttN;+JO@+(niKfFS56WW-OU$49Qgp)+ws-M?~%<&cS0j4BkP zv{(!;kIg6-&Z4?3d^U3!TXo($qmgbp+f5M{l1KNz{tU3SNsA-c^coWB6=vQ>8ALJY zlwSU1za&J4qgek4e>hD^Yz8-^fI>~_8;%L~hH`vmls9FbhuQ~nx7{*mclE^;gihKH z1N);GJvU6+Ev{m}Ar8(jNn_n1ANd|vkC>hqzc+fYwx`z1g^PGqQa0CYlrBS0 zIKsrHSQHsoZ88rkSA?4CU$=0bZ$#*bO`ZWdK7_2t4w5qo?SuH1;a7{%qZc98Rx{?| zm-vo!V*_nxQ#6nm^m0rQ1L*Kcdr&e2)hM)D0q(g~toOyDi1Sf3c>aU}4YAi$?1 zFxBmo>KqlJ)td^G_*N+(6UPwE3@T;23-zU~{$qnh3+Hx%m&#w~63d;E0VgBziZ8nq9} zubK~Z(hEXHDr9N0vAAXYw;@UqH9EEabZOp&TK45`@SOdx1|PA+rq}_4 zE-%NQ^K+`CbEFSb>zAWaYMx)nmj5kaV-8FW;~u{Ve|Fg7Bis~myfct!AJx`0lsQS= zoIhIX1h0~-;;KSafo*mbdxia!7uA`uXh(AJDkO4R!#ZkBmvkC+%1?ROZebjAEAGMW zkJ@qr5uoys5A*@LBgb&DbJilHS5SXw@RP5+tx_R=)zap5zlXreqaPohekI1$rL3{% zKO?P8btMriT*qVHQ5NTbP2t7Pdh6P+aFjdfr}nKwO+x_#Dx~MMl`d&_pWw4S1)6}& z%SYxf`G}NtDZ(BxuNn81ORvW0ug(!3EQ*)6BK#n~*Ar5#*R672OiIqsKD{I~bf;Bd z?%?nprIg-iarJrGCd)QFf;h)@y+cx>V@QYmeyXeJ)z|MB>Z;(?xsJNFCaQ+UU4PBd ztn2}sC`y#p=y?EK-%nS4FXce6RA_M9c0{E_t>Jyyi!bw+OXAUQL?WKbu zN*iZiXaqGbY?^Sx3qTJ^?J-qqxEB1@(B7sviSK(`N?Fj<@NiQ2Fdp8m4oSC|LJ+al zSJ?^iA&YKxv{lsRWsUr}=O`qJx3*-}4ePBvA3`#eXN}U1NqB@095OQX)KOO}JoX3@ zg#5AqZis%?t>Q`?IzucU8+I!YeNc{putOvskXz|%8i$EDaaV`db{QU#bx*NkTv$5dN0j)F8pbJu+@H`e#fCVgTTxTKO0hB1_$!u5ImHq_cQ=FLP zi@H_kjuP;~+a4LQy=5n-l;vC;0F^zxup(vP`SsjO@+b?g9AgM3pfvKH67kNGlnf7K z0hR*EeS6dLk{se#gI`}eH%YJ1^jZ`P3Y^yeyE>=-|D9I%6`y8T4U|pbHsU~(yOn}N?OmTizy^kZc6wqpJ8-UQ#vq&=cHv3)<>merex$EFYkhA7R;O2mN5+eS<-yuT~Cv@bRy zh(Zc?az=SRv$$pgLHVNhpCFs#Yz8TbhSQBXbb?o+zS zi5R5l(Hc*;(1?*j+1&Yyg-zS#C*2?Vo8SLs{!#l%g%5xbh(Y;R>lbqeYKR zQstcHie?&Ls7yc*m2o$%@cHMLa)4(J&!x>I<|A7;te77xYvE2454bc#IMgyy=lMqU zY99_C0$#Fb3bT)&eiMCG>Ol2`vz68R%*)$r2;$prom=H;TRTPh&VO}1=eEn40o;qB z_*BUki=S~)@{y4eDfFbm#kF-3$xf1P$38FBbStXydE+{y*cn=d85q9`H0v~aj;s>7 zT{&ivXAwOC>?kI+<;x!wHF>I%EC+wrYy`DQxzjqZH{c&}FoW?Zd5>14JL7xqyryY= z*Vl|!jH4ri9&SI)7T=_S?Iz3W>V#m2r=5Qt>;+f_)!|*v?w|QJFKU@)9M)%y{a-bG zcRbba|NiTo!#TF>>`F#<_Kq^L_ZBC6Q`z$zQk0RBS!N`AC-Zn$WS^64a*8CxiHywO z?el&7{&v64y`T4UT-T-Z9DX|;OJ@7{Xp@;&r{C&PBScL&_{Ra9a<-Y^ykLz&Q(We& zf2u7CmGD?(SE|VVG>!(<1wVRuIitiH7NHL7PO7~;3rstH(`l2vKwTx0#> z#m|ONn~AS&%V-pr>O#X2Pf$@qud?}P?v9BPI-VKifLv~Hw!g6<56fd-G5>#$z8i)PAe8QU4Sg-rJUmfJIZ0Dl@LvrNcTbb7xedN zup&!OpiK~xDAai4ZFOOGXCq%F;VGs{{y$l7*g?MQyOXX97{07( zIBv1ddzMC({w+nS3$CsEUzY(0Lob|`V73Xm7>n0elnDnDtRUk6o6&q0e6h?Z$kk&r zxz^?CF6u<%q%q80)s|e9cQ1|3^#bVmC9csFOE#-Q%-t)DhTaIk?wP*%A}UfgpD@Gc z(aCI9KyD+yxMtIpT3YB+Yvt{N9mH=-d+8f@tMM>ubqs%swLY1DH6MHXVja)4(aUQI zMw+gj%%77ObK-Bxe{1Mbq-@0m8IRdC2N(YQsY}5Ab^Ap|)Ch{I@RqxD+E5>E@c}rU zx%mr%?|noy6YREDQtF_W^Ydz;%UvV#u9Rnr`u3a=`S~2&Hv=+xqBc;=T4apAGXEqH zLpRKcSKe0p@g2Hhl>0rT(bm5?4i~q-Y3;fNjVx3NjcAU}E{PtO9iAh3YhIvCY?VG@ z`^7^}HeXar?)&1K>~e9}Z1{`B?6gdo-NA2m6A=q=qsMgrRl!~t{R}Vz`3pylVebmh zN{r758+W&XETv6irgq5CzS7&0%^N&x@3u{KT04wmZ0>bYL|s;*XcUMdQ%!qD7;9QP zey5ZEpgHLc#lDc&okYY>FR6U#UrSMR7|h+iNYp22WH;lp@Y_lb z#sMvRKX|k_dY}Hb3k~pxEk6li#6L*IyZvI{xcxa!F5YZF23lf?nd*YPuxvj!GCO zkEdC~6_WJlc`ly`D(;o=e7Di(YC9Q_fmZNpr6-@WU7w@z0q1Ad;w#(f?2aYW`%J?0 zFD8>eMWNX`crUP2!mZ(p03kcrqwqJ1{@10cu{>YFnQSYq-?$CfFoZr3H?eSBQNU>{yXIR*pQza6}#p(zT)H!%uOA5q}1kqK4m< zpC7sY+3|6HnjFGC`t=cGAZW0 zv4Qn!=*8F@!7wwh!$m!R&NZZMD{cK^o&x?W{db#&`8Bk@5`5n^=$t#{K3Rn7QZYy{ zi|c`uF`J-09@i%l z+~Fa}RDs`@47GM+S6q{F#Ju>3v$jsJYUNvWFzmLdCxA5d6*d`7A^TDHSOy12vk-?B z9oDpn^q$_Ln_l0;ahxXoBN?1>ECl!7RR+_(L}c8mrp3>zpi#tQ+y~5Bmag*u0gK(qB`I5yoE<1=F947Lo%< zk7yz_xQZ-z_LZG6v*S*$g~w&LD!pT!bDz%LIu!aZce`@suqoVV@{oH*l)d>%08ega zDmG>vhAMazKd^F)`&43cFVvpMkIv#bZ(07< zGl=~B=0i;=y*8U9QUP;R7FLToG4L(zI=I@?jHNFreRAojV{7-8bLQuv0e6)M)#eYN zB-dB&?*y5&H0$7-C3Fx56^)Rer(Xr4_540^Mkp(50TIf$(CWd%u@4e*6jclyhIVwC z!zDvivUD&y?~e#tS9ExeH!sRphdkgC@>`^-*b%clBBT(z=PyGZ!$DFXxhkse;mRjZ zJ`r3TCa0qlLMNbhHAyaxYal1r693k#r&SWK@moM<-q!LEQHgR0Rm&yMHdbncxIM=f z42XH2+Y7J*#jEP9`?u6RK+nFi0xHjCq6g8asrZDg8nP?dFRc{Dco8Z|OxwPDHd!;6X zs;yErr3o2}zuP}bu!s_N%lJ7SGtaZHKng*Zwg%n}MgrhagA8)tS(;ul+v|r&-a8XY zU-&v}(C;-9c;=o`C@06hlls_kU;1u6-rkfEucf4`e*6k|)53Pkf{6cwxm$NJq9)t6 zB~_#L?E_E*-}fnDyOPAPGWo!c?pkem#m+ukC@Rgas$8Jeh(GNn zOz)0iA4+$*S00n|!+yVdPKIH`N?A|NF?SjbiSh=(QQON3+}ygA4L>mNL7ktw z`NOccggUBC`6XQxcNl$Jo_Iy2$sX<^_J-t$|^q<`Aofs!Ywa<-P! z?TO0%A&SC7Usf-p$=alkxhwomnFa{tTYAK-=7rZ%{LySbRL$C1#1C9pznw#`^~4YW zY@xe&XGM6HX2&B{lBK$zzDPC_w%UGpni8^CkebOpuq1(#F!|0gu2SBvT#WdHXMeTO zF;JB}R0M_<@x= zH)W(Zi=bW3c0cE%0@O)|s!DHeTaY2iy4|Cw)4(^sF7f)Uoe7)4oBk))R6v~1@M!D4 zCJ#s-j+bhDNz6@O%(QuXLQmb(XE15B%hVjYP>oJNv85b`f*R?-8rqX=#rg`_t#wl? zQ0_Z@7FmAQ90}rUwMU63RHt;U?mSl4BVC3>CzQ`?*4G0tir~%m^9GKoWSR7ByEHP? zc&*cG?+-Rk*i5c0GMO-1Tc#ChbDV1HsyixAu)+3z1XXu*^1juO)9;b|8^QHN`|A-a z6nh3xac3dE#7=g@{e;%mQbI3tgo$ggl!#wm2VCR z2hP+Sw++Nf@N}*AW=zfXTT>Mq^B*jO?J*o%q5$t*dxId*9b424@)pL+><+M@8;wHO zZXoVWgx0xr9>yw5UQwcujvUtf+qP~E&^qy+fp6M&l0<08W~M?|IMy|;jS@UVPjgmA zss_f65qG!&brLQ{>DrwB`y`o1ekQU6A=x+?zM!u5CTC~VY4`NXbCQO$Tn0!Nzj&DH zF`-<0>%N@O2~G3krW4h~T<%*BQ>|Ot9C5w_)=tXB9~d(uwWI2kqluSWnSO-TP#Q23+TY6LlFtXO%fm4E+gV^?Q;x3TBi zs`q~M8}4RD)d~ViD4i42nKX-*C#$~Q%@K6mU`sALH3hdk=PL6P&ob;K=r|3T z@AhZtSx)H>mY~aZlS&f9Lm)eAE&pf7twA~vP26Go-qZa&AKCSgan9N^My@b<{FLqt z{W#EDJn2=NtBQzbAz7S3w|M(JiUwU_cFPAG-V3uDE^haoVEI0`9o{G?0jYvD>w?-b zcP5Ni`Shcn7F-cQ!)8!5Ue4|W;&10WKPS-hP-yz#^vjiJ)?~ZMfXsT(ELi6pa60u! z*z=lKk4uJzav;^?v?bf{pdtYqCs@p5IT|_-bZx;ysbrA>hF4>b(?b!Hu#W1tvA$Yx zl7s_Op}#$`8ua4sNG`552%jwKWIKM&B<|9kpf<@eEPAE0%h5g^8q)5=8yLA^+|F0m z2qXN^(Tq zGrdHzK^xdA3jI&RL4N0N)-W^NZl(ygX}^Q!Cs{Uep~o8q>bUhF|0LSdV318!VGLsk z&XH;So{bV6H2d+*Uh(;!wG0z_D+ax|azXS%Nx9&Az@}=N$_F?_k>YPS9{p1{5hjc^ zJ}ygp)!a)mid5ZRcH!&X+?X8M_2AkC0M0zs z_*r5&gOw=!#n$*=u9Dl9Fzx~2PPXlPIn9(2$7dh!oNL?!zhI0QtIQvmA z<eAWy3xYHZS%Rha8eTt3~AR7szBMF`O# zgDZ!nKjdt(@fUHsJJ($Q6`x$yx8#l*gj&%l?( z?6gsw=K)RImRIf(9l$V(1NWREP+Te2Q-6kX-vTU8G|+vpQscZH;}I!=@*nwSr*)5N zwbB2V4b0}MQle(q@j_M9MBEW0@CfDawOJ8p$VXtAX>=bT&l}Ag;&3C}wJh0UV0kj_ z5nI(8!TkB;E5q8j$>MlPNRE6L%7m#=9Z`fyXCuCDbWAK_0HgED|EFSDcb!FlcA57Y_ba?ebv-ts41rMK*9%on4M z;yn-eL*(F1!=}3Jei?{hsPI+<@VNbpBvqmqK-O#fXrj>WIkwWp?; zw2j&f)&(CCYaDvonPYuugHT}Phr4;dXmkjI+%tg!Yedv3`5wi}=p>%I%q>8|A~W0( z;WTS`*mh8*{>4X_`q|mK4v8}DxHm%zOr5vwMtfmw-{WL#IVrhPcA+L`9p9WbCvhfl zZQKMzVbBgm>t)h`R^7!JUpYV~W1QujdUP^*f~qjkx3`ptbG33G)H1e?ArIqMc<_ar z>;-^U=%$c;P1-oJ59SF8dCVh7?g*C@sl~HHn0Av=*|phz+#P~O242H6(@le#x6wcv z0wFCJQ*x3Kq%H_Q18v>m#)V>{0ZlTU5@*`H@V0@&ijR}hT=o(;-h)Hs%#0Y1KkxRi z=P4Z^UqPaI1nsKe*jEgD*j!NP77(mLILX}0E&qqDvJuZ<#8ggo!vU^;# zy&i)@DM#3JE+LM&Bx9PauGLl5L2YE(FD-C{^2@e!U-o zq)DQCg|Pxv0Ur~Z`vp$!KV+3k$soX31=pVHyUkeu!;JriTg~u!+m$norqvaLwbHV5 z)ngS|xEY$Twk-URt$j3PzCJaNGL)?_ z;N%$g)onD7YK&mm7mZ&VuyT8El7itYM(EBR;7(kxmD3LH|Jk-Et9anvlEKUb;UC~C z!H+y)z|mulL8~c-@|NfcpHnwI$j&^DhBYLG8^zuNKQ+{jsb9LMV_#;7ajWn`yX!~D{s0r_ z3X;Mh{rQ1%MJfS%+1Upj8ns@_kdKuXskH^!b_rNPZ+kk*M~ip~-M*q8*Ie?nd-2nz zqD2`y=GWQsx;q-E!k~TWKu5rOYT_X2oKZ^QeyDW~HJ{U9w;3`8+5c^J(*5Ku%*ls!Rcbdmf(5za$Q`l3HzMzd8!VCVc*aBzaG9LlY^hB|8HDEW^l-x zb*U87YoMt^mIk;JsK}z0euloOfec86?d)Q>dlMKjuL))t`*(6>j#o*1J$GEanA;B1 zBK(c1+Wc90{&&Q-ZxR?GyczBtSk9?)#Juw7fef)H$J1<68LjtDnc{2m68ipk8&#~n zz=+Z~RnX2xxuQ0ZM^Z{5(~2W)6s4 ztR|`!fi#9&3BoH~6(g4D=WJfa4v|DLw(P3NkNNN}Izu=6dvpkKlw2a+EAkWa7qlw$ z$=ZY5eu{l{4q(bNE@x971dWX#2xP(m^^9?fr1m=1b`vp%XVl3b|EFCz zlI-^9Ca^LSchl7WFkuvNC_Hz8tOqS9xG2XjdM3Qa29%r*?wLAEBF9mmT(r^uxN6d~ z4$tm(W;^er0-#6=z2E}oA(?zcN03Y@&c!RigOH4C$4V4cg$gMNXS-dhbAFKa^6me; z&dhfDfpiEug3tf5-F{Ha1n-7f(WRDJ=99-Dxe^}f&fnifC2J-bRX&4(+HGdT6$sE` z(mBiQy;TCm>VWDg|6TdE$SyIZC#;+>Uve&*^|(iSK}B7#>M)HXMukf9v}gB$qj-&` ziUoM6%^ym>IF7pIz@V2tDmY>YuH>l>9`jd6_CSpQ8P9CNzca)%|>OBM{zm-=up0%Y&4m$l{WxJD>Xj%yW)Z zgfnOF&CEMYL&NWI04=i25^`7}k<|heurHCIRE1QU~5A9JNKQx;hPadqO z-)(?e5&KF9-QD#29D&NKDhg!7Dk?Y`UYwBeXi$8I+#czIruJH$huTCy%sD7&TqtA+4EZ!s4|iW!@tdXjIG6g~ zBbY={4vVfJsW1PesDcvmR9do``z88cKF$ewfG~S!&&gWr`VIm(MxWtoY!OVSe;=g|LR+v$gUsEn@9(Duz2z7^;fZ|Se zYS5t?04aoPS+LacDX~hgja_Q`cknL%q#l%&g5?o?-&TXu<*qmH)S~Eg41+~An%$rR zq@@1B!ipOSB?!&mqI!X`Yqxvx8=o;=ii7pk+ib@00E4=-#uj~Ck_9;eR@K`(cQfMt zm4|(lH(?NzpuOx2wylKFTU<}5&$dl-gjvDe_jW0HmK-5)PvcxFu&#;KSc8;k5SV>f?oz=KeM86*SUD=3WJCWR;W zRCHJxI?khiaX(G9)c=xF&mHnW+CXN>D>B92*$HwZxwaf#?;&I?i3W#G)*OUwg1Q)T zok2zBIZxe= zN@~&W!c(gIbRwQ>a|6ol4`%_pdxwG~38DtEyF59F|L;@CH!;&xX)C3->MkoR556bA z1)fB^&Eaz06#Wvb(J|Z&TrA$bH4ppHls3t16&MBuUIn#26Tq#76X#Q6>X|UU>qv)0 zVJX|-VVz*qhUWac4OqTr^LBxTPiVq~4j>2RGO#~$l5u}7MQy_tRzpAmrK6WACZ`y& z5=8y0AuYFi1<0<%)pN{Nyudl6T4cLR^|8uZE&l5bj%Bh7_Lap~a-%`Z%kwnr=eaed z4|E~z!U|WPWu_EcHKtA35&7;@4^IqS~AIui%I#l&8CkiP;+g|G{oIlEWixgy*;E+YY3T2sZz|LGO7Uw1>hQAL;)7Vu-* zEU}T;JJZ@ORuR(J6vF!5F%`s<+HK5x0$rdqh7y-f&~RP9Li_>(Nj;*4oy>kb6NVxd zxvR~I8-l)aa;wvgkON(X|Eh2`I<+O4ci`t1W6-fg4{#P3TW)Z|2dk)?NAuZ0JP8}V zQCpB%Gp!6u+?EOy4OIHdtqORK|Md?NE?m!t8zqCG0)m>`d+Bdv7sR9iL+RExX<+(}3=5SI zi~P7W5Qgeck*@?lW#5F|oJ{qULvOFBi&~Es&-_c65?jFWU^cW7Z_PIDesWJ~rGH76 z`|^htShAWt6Q+v`{FZ}yD?v9|hO`sv-GQcA<`dhM;PhW-n`0)=qQLX~+-}*M)&0wo zqDqM`R51CA`Hy7A)`($J3R{yv(d;AX23o>Fw_12YUHM1lcW0sxD4g&U*H(6(P;bJ7 zGV5`#O&^bc1D3UghkWA34(#dp5*UvXW{{wJzEQZ1Lm`oi`aLjKHjWx&?;pa=op019 zS5tDaacics7M0xu8UW6$M4rD#qs_tqsq5UvAx3$0uJ&c-^lK~wL1W1i83>83hqzlo z#zjA)H*~sRLd=sEO^T$O#B=}y;jAobJ7;MYEB&`0s*IZ^6X-O89&I{XSRZ?YfgGC= zq8F&mG=i6YQNY&9|1swDV&F6oo>Nr8hNSRPsxX7cbD}|rvOHJc)KXKh*-HCX`S-Qb zbPXVXm-QH{YD(E}kgH>pm{w?rwY&#A=+1jStb^Syi4TOEs2LZRN$C_KZ&*d~;r|hZ z59DTOysTIYkS7vK6yWdf-CAM~x`D+4uWzVAU1ZQXjV_BrNs+c!`GZE#fS+&$XPtbY zU!dqjM?g`WqUB#N;`&)eCr~bF*j*7;{(uuGEYhFppE#$?#(A@#>}GVZ(r49bT+YhB zJmxHTP5wB(X3GVWUSDK`CPhZ_C&kK^4`HKUO~&g&ZHz_8~nf0>#JY&)Mwp5z!8EQ=g@%!KixczPgulL`oob3Dz`t2WB|bmRS{o&G4oES!Kg2h!c9qMwm;N@m;L7Ic zya8|NtG+Kbp}G7wmR%{LK&MxRG+p7Td2U_w!>D zyxo3-R%1zJ^}Fy?xPJ<5Uqn5x(q}Icm7ZO9qA`VyS{o2-{5dbuQ%{q-22H7_XeI$r z{)Lp%MSiq7@mGfgmjw!afesl%WdQX527FlUq&kI^o@j?7aY%2vncz;5K$SobUxkTb zP2%;Vh>Z=YCezP{a8c?#_^cVRhxh2g?Rfde{7l&P>m~JVotS5UN<0*gOI`WrJt2Gz z83OTuXvo$PV3bmjUmunI7XreH)#Yc~;!F3lpv+xXA`15k$fMPjAq3%BKodhaA6Dl+ zjEsr1qvwSmvDlnrH%XvH#K4jt(OuTag7OftkLf;8{j&RebY=ML9#;h~Ls`WW?*rg5 zDMpMoU2^a)kQFJ;zn-)zhz$o<^Xibx9sQl_+nd8H>+XA+@R*j>sF>drrQyZNY=ON)%}$U)YmlUXgU zO(c~v<%5|Nr>fuxlJ$Z1Q()XOUnhA|?W(R&47W}N>TMR_@?G}@tVm>q{=r|Ge-nY# zn)`u6Vf%m{R{S1hlkW%@*Ix4qAnPJmVdpS?_nRt85eT@1af#({#U0$X*-^RAh>L&o ziAWDtBuu2pl^E9KfithK9w+BQ9$f^4|9%?vP(ExL?DhAb&MY0tdO8L1sRS#aC#v5s zE`+#729$v^rE)a16ZG}6p)b|g|7Gk+0NX_gjA6NqomM1`iwILS3A$>kN~t38Wa^1LKHbZ z*;y47!70_}LvTL{cb~J2je{bGSkIlyKu4A3qX`-T-XA|)L|94-gA>fiXFT3g2v$DJ zvoESgro#86OI(Ij0Gs3D(g%8&$|dffJno6a{e61V4}rk7vivx^me0cK0_f~#^?PcdF8Y8WOAj~{l%g_kxtjkffQJ9G^-BG|_|btdy# zQOrHX_`G)>Io|IiC{rvm7uO|ND|hDL9LO*#z!j*X)1u=_!>LUN|2QGoio{<{fc&YI zWhKm~M470Hzcm|H@lPOHR*6SpMb8uH_W80~r2p~5ZG?@jqD8MCT8hJCj1#E$nsS*S zY4)^{h|Od@#(OgE6_!_RnET z%rvg4h|g)ZkUcKNgmYRJzx-c=0$bQMgaP~o)HF9Bba9GY%-D~PhB8HnlNuX~kT32z zi6G|S6NNufVr&<+OLF2YO~m31@t-w1S$ilRDcX|^i5b|aVkXQ`indm5i%vfv`)zxt z@17IAE@-q-6Fui}n9ahTp$L=oX_jkB546mnsCt;mV0GOvWtI00ug(!yMf8H8Aaqf? zc(FQfJmwa!pXaS-Qx*5OF*?9=+ru~ndS`=h(S@|g|>%&-Y%Qg)? z8OYdtjcq(UAQOaiy>28eyZ()`V|b~y@(Yo1GZ|RA>ox%*rUf}HI06pcHm#YK)PB+nl;zaasLlj$;i$C literal 0 HcmV?d00001 diff --git a/assets/img/Mobile.svg b/assets/img/Mobile.svg new file mode 100644 index 00000000..a7b773b9 --- /dev/null +++ b/assets/img/Mobile.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/flags/AD.png b/assets/img/flags/AD.png new file mode 100644 index 0000000000000000000000000000000000000000..d965a794ee2d1a1d3ef87f873c7caee5de64a2df GIT binary patch literal 263 zcmV+i0r>ujP)YYL++@ItF9=wnVA>k+?-o$%4g~+ZO@;vN z|A8ej|KFb@!GOQO82AIk4-aepe|S(69DpD(lG6b!8fl-920k-Uf&tozk~a{vl)3-_ N002ovPDHLkV1h23e7^ty literal 0 HcmV?d00001 diff --git a/assets/img/flags/AE.png b/assets/img/flags/AE.png new file mode 100644 index 0000000000000000000000000000000000000000..f429cc47a7ac7eeb00c2ab7f0ea91a138a7b179d GIT binary patch literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`MxHK?Ar`&KKmPx>XI7n|bhd!` z9OtUP?#FmGOLQDMe3L=wK!RxlqlOQUoF7|26Z<^|hC{P6p5A_!b^@rI!PC{xWt~$( F695c2A-w^4+-%SZ@~Egr~j8CeE#1o$^3skH~Ig?aIgOt0-cFB z;BsQ{|7+>d|F4%O|G$(F@c&v??Efn%p~M?-L{{Se-L8uNx66wD-!3iqf48R!EJjiw zp3qSKe?2Af|Mlef|0lFmNee_+6z+v$lB1Do35jR}20k-Uf&sHPDk%EL^fmwh002ov JPDHLkV1l2GdH?_b literal 0 HcmV?d00001 diff --git a/assets/img/flags/AG.png b/assets/img/flags/AG.png new file mode 100644 index 0000000000000000000000000000000000000000..6470e12b4ea6a3c2f8cec251814db59ef40fafa2 GIT binary patch literal 302 zcmV+}0nz@6P)NklO2X4SQ zLAC#tTw-tq5+Y3hOA@&LpIR;i#vnFG3?>g!3(|vPz!@MmVB!4F$z+xadkQ$I$3BnFZrXaES`@{IhC4nSh)>TntW!msBpgKGw{v8lmp zz(slE|G%ES0AmoFWCK9>UTiiP2(RK$<~{kqrRxL52`z z0EqSX@8ADm05kv@fE30R&-x#gH|_tTg=_yu=1%<|S2PRF0FWjamXrIB%m*p_`3q?H zjottM9Gvt2|Ep_YK8gV_|Dt$d#ftyE{d>U*AK!oQ|Lg8)|9@ON`2X{|rvHy_-9&T2 z^y&Zo3U{CwP~Ej39*A$BKK=jiz>NRDw|4yhe`)joH}`L&8h|gLKzPRb6aPU1HZ1$k7`^B}gFzW)G!kV1zH~sb e0fQhjQGfxn3Zp6uB6JS`0000$bS|FMxZPM7=ze3 zK(XV*7;s8Z^?yFl$p$Q(V2vOQV&?-5I0ZDEpaG|W1~hR?{IB8?|8K?0{oj(63(N+I zf#e7pa0aMoA+Oy3b^HqdZGi?@1Mzww8zgpy*l4^0jI!%O8viHqNdKPz#31$+Vxtim pUjk~ngheCK1`wSR2R<`Vf&tP$Q3E`%r(ysA002ovPDHLkV1nnaaj5_R literal 0 HcmV?d00001 diff --git a/assets/img/flags/AM.png b/assets/img/flags/AM.png new file mode 100644 index 0000000000000000000000000000000000000000..5b222d90c7b7884c98e7012dd1f8a2908d70062e GIT binary patch literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`2A(dCAr`&KKmPx>XI7n|be4nJ zLBz5`Lh`_Y4X^AiD+DAz)T>QUV&_+9X0tO6Y+!tHvfO^c1rv}#44$rjF6*2UngB>K B9T@-s literal 0 HcmV?d00001 diff --git a/assets/img/flags/AN.png b/assets/img/flags/AN.png new file mode 100644 index 0000000000000000000000000000000000000000..2c9e769ba8e26ab7979f262720e236b88b4b9e6e GIT binary patch literal 145 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`5uPrNAr_~TfBgS%&#XG5F^Vmr z!TQlA4GDG$9X11lkpIWqgoUpC^Y@GUET3oRVB}WYi7#-KWvcr|9Ou%m`#!aAPpdm&+29V zKS-Ac8$^-;xBV3Vzv!0!e=k}A%m*2ESxW`G0YLB{9oJ-Y|6etU|Gxk)BN)FpAp)k6 z)!;QCHh|-QZY<~j)sy)C*JN=44dcU5gVO+zO*eN5f$^!eg8yf?^8WXAW+&bN7}nQd rfjc`6=ps8)R?-Y0gsGPo2Gjrm;oLYvJwA}200000NkvXXu0mjfJm6xO literal 0 HcmV?d00001 diff --git a/assets/img/flags/AQ.png b/assets/img/flags/AQ.png new file mode 100644 index 0000000000000000000000000000000000000000..565eba0f20a796c113d25d83340cc5fe9ce44189 GIT binary patch literal 382 zcmV-^0fGLBP)z#)_U>3Y2{h4Z6L!Qz4`GU8JxNM>3`9pJ7B{x3@`=? zW&>?pbnwN0$EFMa3l`q~|M%a&|LEYtgD(*8U~_>v(2095zx)5@^RNFufB*fD4Lo@D z{l62?0+3paFwieM{l8@K-T(N&U!bFA?0yPXi(vpLfNECW`~Thz$6T1^oN>_y4ai?|}F%n2iHqH{jpjKmR{HJpKRa!HNH$ z?(hHq@&1Yb|Ni_Y-hf}9UjsEy`u}vPJrLXef3mOV|Id#vi8tWqr&mCmd;dRQW(UMJ z|DWvX`2XYMGt#{9@&3vGPxkiwf4sZ>|A)Ir@q1x-q=Ny@Oq5^%`yVsB?7&ky00000 LNkvXXu0mjfFxhJ5 literal 0 HcmV?d00001 diff --git a/assets/img/flags/AS.png b/assets/img/flags/AS.png new file mode 100644 index 0000000000000000000000000000000000000000..f959e3ac2742a8d0b78f10725c61f082bea00568 GIT binary patch literal 448 zcmV;x0YCnUP)Hn*(JO6)s_3HnZH!rappwANezmUuJ z{|0_Fux)2O!~Q>>webJXSFiuy+`an$f()PkwHA{9@1H-4FhHI;`hOCK)Blxx8emPw zO7Z|dAvIRsh1Z9)Bpg0pn9g{p}Rf+0000Ar_~TfBgS%&#XG5QHyQQ v)h&Pg`Q-fA{;13Ih%vh@E;L9{^Dsko>_H9qZONs zmBHil{(7LT`@b-)W}9=d<8*&YW#bwH36`0i bOgs#$d;8T>TDR~5oz39s>gTe~DWM4fK6_lw literal 0 HcmV?d00001 diff --git a/assets/img/flags/AW.png b/assets/img/flags/AW.png new file mode 100644 index 0000000000000000000000000000000000000000..6ef2467ba5e11cf4547d8521eb34b6ae1d7ec8c3 GIT binary patch literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`wVp1HAr_~TfBgS%&#XG5QH#yR zNxJ8(V|OqU0Hj6^3URCL|RZe2p=X>TOmu4s2q4HMQ0C SXQ2nka0X9TKbLh*2~7Y=E+?Y^ literal 0 HcmV?d00001 diff --git a/assets/img/flags/AZ.png b/assets/img/flags/AZ.png new file mode 100644 index 0000000000000000000000000000000000000000..b6ea7c714c50a86484667c59ec01aa4857005638 GIT binary patch literal 267 zcmV+m0rdWfP)CvG)IOGuHn9^XSF@*L=1>3rz4E0CK@=9_#JK~28RFNDEa;W(m3n?2VfYyA|?zN)}{U@2M;0x20k-Uf&s=IjIBna RB{2X1002ovPDHLkV1nFPek%X~ literal 0 HcmV?d00001 diff --git a/assets/img/flags/BA.png b/assets/img/flags/BA.png new file mode 100644 index 0000000000000000000000000000000000000000..570594bb173c6ade3153b570588080b774553134 GIT binary patch literal 355 zcmV-p0i6DcP)WUI4h)pz)$RaexB*%&)BeX4tp6WZ zvj2Zfa?$@+2Uu|%g3SOn)z<%l`d$C$t~&qUGIZVl;F!Gs?~brzHw2pjFaZYn#{YeD zPW&(JJpJF&H4(cZ_zd9EX#X!~IpKdm+KT_}GcW)549djx4n6}wm_@nyzq;ep|3xi3 z|GOt{{U4uR^8Yn*z!EY5ggMpQ{(l=Ip4sNzwbZm!=?nDq{*oc^^F=5lT#1;TWR!P zwr9ryi6d$^87BNs)MMCqlFKE4VUlb|v$Cu}?tug8YF-8mwZ=t@LtU5V18rpRboFyt I=akR{0E?YGDgXcg literal 0 HcmV?d00001 diff --git a/assets/img/flags/BD.png b/assets/img/flags/BD.png new file mode 100644 index 0000000000000000000000000000000000000000..fc7affbfa97168d30ebd7bfe795d733063a0bf64 GIT binary patch literal 211 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`vpiiKLo9lefBgS%_>1+YB z#HK(0CO2m$H#ki?|9@)Py8rfB*8l34KmJkQx%YMb%9Qv=JP-W7|E)iI;^%*HU3Zb1 zV^@CKN3#jCrTnZ+$Y^_bBEkOgSms4p LS3j3^P6FXn_UsJij)PZ>6CM}~t4c83ZL76T+?w_P PXfcDQtDnm{r-UW|g`z$o literal 0 HcmV?d00001 diff --git a/assets/img/flags/BG.png b/assets/img/flags/BG.png new file mode 100644 index 0000000000000000000000000000000000000000..903ed4f0c7c7b89aa2d9015994b51a94e3de6849 GIT binary patch literal 103 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`dY&$hAr_~TfBgS%&#XG5F^X*h zcUvzb&*e}5CO5NBWW0W*1;}Wh%-F1CJh7KCIsJNaT#4^ApiTx)S3j3^P6K=dle|)O$6Uh(XPhbCkU1yJ5tihq!4|SjQCIvq_ ekgm4Lkm2H~2O_WfS@?lwGI+ZBxvXO3=u6DtVJFetrDWxW%P~2=SVK>|;CEQreVlGz7wUibaCPQo}cEf66 zthU+gx1F7x?VR&<&O19g7=;VZ+qceneth2NeS2O70L5>l_$vUoEQQ-1EiS5*U+Sh~ z5XD3auT}!s!_Kk;;W-ODHFXG#P9V9qfyAl}54tf@9Gab%^7bOfZ-Cm(MofDb!0{Z) z-LM=B|LGwD9v3+9V3TR7*yyuDtdRjkb-9PYa0QZ-V<oo};Jp>&$x=)JPF<{9 literal 0 HcmV?d00001 diff --git a/assets/img/flags/BL.png b/assets/img/flags/BL.png new file mode 100644 index 0000000000000000000000000000000000000000..533cce919aeb52b42424efd8c67b73d94e95383c GIT binary patch literal 539 zcmV+$0_6RPP)d4@8%3+SU(;ZTb)_+nDnQ3|g}p zX`#(DO=*>zZlP<_v3{t}Q<}UxbP%a^SV3R-oWAfne1E6_)!#(*uYkQ)H;~C>sn@@u z)T>m>X^OAsSbOu3kJUV_W`p(jA1D@!zXF9q0k>PiYPm#ZGr?*uh;QsLK1UBeM-Ow- zDH@+wdA#UC6h(Y~Ke8-$0KealTCL%_eUMznP5$k3yc7L|e0t`mH3TO`HcBxT7j9xQ zThQrr2Gm literal 0 HcmV?d00001 diff --git a/assets/img/flags/BM.png b/assets/img/flags/BM.png new file mode 100644 index 0000000000000000000000000000000000000000..5b66e1f697622e7383ba3637b5a768f0ae556722 GIT binary patch literal 321 zcmV-H0lxl;P)i3?a$@ z5bN*XzyHAiXaF)G&H#`m7?zX!kIV-tJh^u5{})%U{a9__kV6>=>MWbkN-Ux3IBH#kHXNgA8JU+0jJP*fZ;c+0^J&)q%TiK;S67>pPeK({)fhy`N*&J^G;Lq=vB zVgQ&<5wWj;jDdC_Z1uAG$UkP3z5_+Os9z)W*3i=>L`xi$E3`KEn0t6iaN>bjfZfkR zWVRvJ5EVh}G^U+~a|Hqynu~Af9&AA24(50X(F#GQj0R26lc9rRu|O`DC!f!8^+J-V z)Ev9-KHx7dgCA0UdraolYudFfCQ}orzlpl5!xcnipy`nnI=WQySg-nw9T_|!MtI3(ryk(gQRXua&b~qW!F?{?yUopP zmics=cs$+%@I9&CPi6lwWP3$f{-`*s<6JkQ_R07*qo IM6N<$f@QqtE&u=k literal 0 HcmV?d00001 diff --git a/assets/img/flags/BO.png b/assets/img/flags/BO.png new file mode 100644 index 0000000000000000000000000000000000000000..3f0c41f7dc839190a522637e217fcf68a29ac631 GIT binary patch literal 236 zcmVJ|yBK7~v6`}t>o)T%m zuUDe~UoPSPKe0&ne|n4e|AU!)|9{*98t{ZzF97kE2crL9oDuy0@}$83w>N;|kMV>7 mLzV4+a_}HBVBj+oB^UsIQISPOuC$;40000-~{+|lOAU0TxumM1Yea`>Y=9vCpv03E* z^@XB9EcSoZ7LorNKrxUUSPfPKCOG|P1lp9dQsw{MZCwBJJ^lVG@D~1;;mG@+?dglYgt&~%RR|6SV!{{KJC@PE^^!2kXcbN{EM%={l2G2y?8bo2lA zIP?Gi&olhr15{%+#|Uf?+yI?frvG0ZWcmO9Bm)>Hd4&B}60Z1fq*V6b&9>=(WWucf z&1HH2{~d+Mzd6YKUw4)Xste44rtaOr54NR0!{)yXU)_Ig$)f-2A_f1YxNH9}ZT0>C z{~W{reLyu9U>9Hs17@ILKG2`{fnkv6;s0Mrp!mP4Na_D#zmWfTHgWzh0LBPN4MrHi zV__mB7C<4s3K$F57Kr`7I$!MnY9L!{jtL|dCOQzG4${^r|Ib*X1c^H0(gB$%ao{r( aB^Uq?rDjeovnxvg0000B>Ar_~TfBgS%&#XG5QH#x> zqUB$DMCBj*!>=Z*B(^X*bR9hK$^P)4yCM<{hlGs&ORBA3dVoRT*~8!GFIdcFVs4l* zbLPkYEpA)?@0YW9c+`To;bCq!?+N1uBk_hq3#tw|9BG;+$ojJ3(iDpiGdqQN7;f;i V?mK9_GZSb#gQu&X%Q~loCIFxaM1%kU literal 0 HcmV?d00001 diff --git a/assets/img/flags/BT.png b/assets/img/flags/BT.png new file mode 100644 index 0000000000000000000000000000000000000000..fe52b87270e89ca53a2b0cf01846d8b97b786ba5 GIT binary patch literal 449 zcmV;y0Y3hTP)%tV zl{QUF?K0RfJHP$?p6B`4J^Zv`_A;-RNn#X^5qZ|F+&dzM5~hHx1{mf+!N?y)_T8K_ z$CiX-;;HH?bme-u#GCha;7-(KNll#3szkL|~gjia)p;`v8HR;N688JdS6 ze#eS6Y_Ut2c>tf6uzG`v=Pwq1k=iGTaWC=46|CJMoj3(=UXqqSVXJ-c85PGLLL@@_ z82QdgVn5M1dYX9uE^+!6+b>_z600000NkvXXu0mjf+}_Nf literal 0 HcmV?d00001 diff --git a/assets/img/flags/BW.png b/assets/img/flags/BW.png new file mode 100644 index 0000000000000000000000000000000000000000..8da822f159b909f9ec1f4d56bfdea63226781cea GIT binary patch literal 108 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`#-1*YAr_~TfBgS%&#XG5QHyQL zx?O+g%h+`A{P4GHkYHf>fAV1S35Kr22^CA48PEAlNnjB5J;MIEs_QUNJAK${pmUHx3vIVCg!00js) A7XSbN literal 0 HcmV?d00001 diff --git a/assets/img/flags/BZ.png b/assets/img/flags/BZ.png new file mode 100644 index 0000000000000000000000000000000000000000..9ae671554023ede8c490bcd758ffff43147df228 GIT binary patch literal 337 zcmV-X0j~auP)4tW8gHQo?Zszz|KP)OU2TaGa)H)Zq4`rldnI&zR zYTKsu>=KRrqJob;Go>PCRU>B~kS(P+yHPl_P0zRK4-mqO-o*Ei^XC{&8S}0};z(yO j`ok7_e@MN5rXYF;-WrKfJeI?H00000NkvXXu0mjf+QyW= literal 0 HcmV?d00001 diff --git a/assets/img/flags/CA.png b/assets/img/flags/CA.png new file mode 100644 index 0000000000000000000000000000000000000000..3153c20f501891c85346dfbabc6bd01206b8fba1 GIT binary patch literal 177 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`6`n4RAr_~TfBgS%&#XG5QH!nS zqQQUu!z~Q6qN1ce{QLX=|JSbqJV}#t9O~^|q*@$v7V$>x|M$P1nVF+alHoD)Qx0i^ zdOIg&E4EXDW(-f_Q*KX;Wt^g{$K%kZU@d#3jZG!<;*NTImdSJZ&dqO6n5Dj?@x0HJ a1cundpC0O!rpyP* literal 0 HcmV?d00001 diff --git a/assets/img/flags/CC.png b/assets/img/flags/CC.png new file mode 100644 index 0000000000000000000000000000000000000000..7e5d0df21c9abad0d15f31ad2f16650df9a0d52a GIT binary patch literal 259 zcmV+e0sQ`nP)Pc83}CJxXaFv}0B9gLwpgGzAyH(&z-J~(FaV17GA7$d<4XVl002ov JPDHLkV1j-kaKiuq literal 0 HcmV?d00001 diff --git a/assets/img/flags/CD.png b/assets/img/flags/CD.png new file mode 100644 index 0000000000000000000000000000000000000000..afebbaa7466fc24aee7bfafbd31503026a0d790b GIT binary patch literal 432 zcmV;h0Z;ykP)n!*+h6Yg$6bzrM^E7*4&&OcQ zz%QZ0mf>^#M?ddjo&&1>w#@(LGY3T&+~Kx(!akXidEQKogADG<2STU#eq0H(l)=;0 K&t;ucLK6Un=QUXX literal 0 HcmV?d00001 diff --git a/assets/img/flags/CG.png b/assets/img/flags/CG.png new file mode 100644 index 0000000000000000000000000000000000000000..7a7dc51d4303d4512d0f9be9170a419d031850cc GIT binary patch literal 152 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`@t!V@Ar_~TfBgS%&#XG5QH#yM zr}Upc&$^p0zy1Git8Y+uIpx=U73K@3_X8)Y-($#`)oI2sJ#7)sf#ow7Ft(UHHf@kN zYr(4!W*o-saj_?nVdbnq9tC&fE=K10Gc^t}WaqSR(5vl~2U^A8>FVdQ&MBb@0GAmw ATmS$7 literal 0 HcmV?d00001 diff --git a/assets/img/flags/CH.png b/assets/img/flags/CH.png new file mode 100644 index 0000000000000000000000000000000000000000..dcdb068efa0f8f183f68195945455120e60f3956 GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`+MX_sAr_~TfBgS%&#XG5v5Ku> x#*zn$Neny}?lZ(R)Uol@Nbp>2F$vfy$WWRwwf#<-a2ik>gQu&X%Q~loCIAC<9aaDU literal 0 HcmV?d00001 diff --git a/assets/img/flags/CI.png b/assets/img/flags/CI.png new file mode 100644 index 0000000000000000000000000000000000000000..25a99ef24f7365c96e8db61c0deec2e9e3b96d4a GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`+MX_sAr_~TfBgS%&#XG5QH!l+ yefp37zKe{9_ptETWF1(+*nFm;@x0HJ1O~g1E4v#Pl&}D`F?hQAxvX4Lo{`2a${Fk@y1=AotssSKPFf1qcADItQtmQH3e?Zo<|8`-s z{s#hS5FaECHvr~e6fdk;@xQl!?|%VAOcYub0E|3orC(ZzXI7n|bhdz5 zLTCEl<87S`E7)Ef%VAF5#PdPV?!*5Nt>ORq=Tvj1O@7+6`nz3&VWi%Jl8B%2+F9BX zpDorZoo7v9e-xU#^Z%0vfy@(l7O?Q|VPu%M?85tnb+`8eEn@I=^>bP0l+XkKw%$1w literal 0 HcmV?d00001 diff --git a/assets/img/flags/CM.png b/assets/img/flags/CM.png new file mode 100644 index 0000000000000000000000000000000000000000..2b4cea9a6750b99faed8bab62297ecdb510267e9 GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`S)MMAAr_~TfBgS%&#XG5QH#wW zdov z;QxO|0ce0e(11kl{|}7#frdd1#$~_*6aN3N3%LIOJiz?_(+u|iPXl@W-&F+~DkJ#+ zMFRK#7fIaz?<(Oj;I4|`|JOxa|Nnns`2Xt=)Bgvi{QvK%K@@_$phLh5ca;SHKab$~ z|E84d|CS*W{~y@{u`M4)Kw=u8 zfkXopKQQF~|E8Q1>;+JG5EYH+_^uj>>44&dM3w;qpP4AZ0FAYHg6YcW1^@s607*qo IM6N<$g3i~HiU0rr literal 0 HcmV?d00001 diff --git a/assets/img/flags/CO.png b/assets/img/flags/CO.png new file mode 100644 index 0000000000000000000000000000000000000000..ad276d074746127ecee334de5841326ec49c62ed GIT binary patch literal 108 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`#-1*YAr`&KKmPx>XI7n|be4np z9D`=jhZT$*%o07n|4nXYpU9YQ@%2BL(X3z`*ueOE;w1G{XDgXcg literal 0 HcmV?d00001 diff --git a/assets/img/flags/CT.png b/assets/img/flags/CT.png new file mode 100644 index 0000000000000000000000000000000000000000..c9fafe74ba399ee72954d4ba8485b6857b38044f GIT binary patch literal 1356 zcmbVMUuYaf9KMu`#+s!Jpy}Kos_ICHy9Q0ziUTq3($!2%%ZprP= zvb*P90@7?g+g7KK_y4N4+T%hJ>>o5+CN$-6yD~iNBoiOUQx$Z zR5S1xiqyzVp!k~5! zfc~0Dz!vf~P_xRmC)ScwgHt5qV40>sBg7w1QmH@;jAVy_j9moyC>PNg2!S9(VIMCD za2H@1$kGhCg$V2uc|l}jB#*{L5jEE+h&e@VYLQuzD*3)6(sZ?2jaIoRc8fGjBoYA! z%SK2<#G9~vtroGp-WG#`Jl!=N-^4Zu7_~86@skwE^wt(ECubH*zIU74_L@c!%jlZs z&@jr-ZX+hxUkjDZZjP|5+tHq%LwAzjkl)KsIEc<64_90rk?`KTq&61{0-Jjfvzj}O zNEP#L1vS>QS@AiNyJ>4>1?v?H*?vYzQUnn-Ohe>2xi8M77@kY8iV9&0#$Yd^ zBq=VnI5xsc@jh9NB_NC`9E52$!7)r4Dx4H!5-KCeEo{d2e9hKTOV=d2O)Rr1R+L?& z`Pj{4TyFK#a0&a^D`5x7`*?7`v<+PKf>Ph{V85poQ69OL*X*HG*>8@@W>7z|tl354 zLBv1>ty1=>0o6qa6tD{%|H&O){%^sGkm+r)(0Y#^@F){U5SqnK-W? z*L(pPs*5eq=mF8(z>BmZ^NPR<952O!8pK&eN->-iS7NFZ=T)kSHU3Zi=vKaW{RRU- zzjvXBoDqLzlvF;xcwrh2g;Sm3#U32KbYrmN#Nf@Y(|dmIeQou@vGZH*J6f+#cN{!9 zTTebQ|88<+OZUmEmzR2`7vOStqV3bAcNP!7qU&#pGcT@w7JmKIO8)WLi?i)>*AG8@ zWbb$3{l6WWiJTbqcck?-=8?`n+LG%ZEj-Vjy7>LC6`)wQcDf)~Zw6UYfbNc;V7hSHC&>!F%oJ Y+g83hwtVH9hlvBBOnON9L>fK*FP*^7E&u=k literal 0 HcmV?d00001 diff --git a/assets/img/flags/CU.png b/assets/img/flags/CU.png new file mode 100644 index 0000000000000000000000000000000000000000..99f7118e80d761483adc35a2bfd222352989eb65 GIT binary patch literal 215 zcmV;|04V>7P)L&@q4LC2N{ePdB@qZ`feDV!|;roe&|9`)E_n%0B zX#facR=4^8Vap!U4S1MUfnrd%v=82}K{4QQX%pB;5RGieS_$j_Q3}a~4Y+3E{r_%Q z`u}UDo^Z{*(%!_o06h%euis8yAl{45BQF*}>A+nnm%Ma9$gczqpac)R0RYTEq&yXp R390}9002ovPDHLkV1oDzTX_Hg literal 0 HcmV?d00001 diff --git a/assets/img/flags/CV.png b/assets/img/flags/CV.png new file mode 100644 index 0000000000000000000000000000000000000000..7736ea1f395791daccb4f346cc6c881b83e5b05b GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`fu1goAr`&KKmPx>XI7n|bhdz5 zLPzbtIy-+6^Qu?1|8KjAPe?y-Xk}W&O$**1{XFlE|2@udFzyAAiZvLNt zr~2*xZO!Hf*e0bg@XX)!<;VZ4*82aAv%)6jF?dYw%l}{R`+nblom-(#b~dbu`0;tp qLwm~^RcryOih(Hy4y3Dj88Cd1i{5CpE8!#1MGT&vGR5Kls`WN-mt5a%{eRvXTTBCH zF1z=iQ3=96w$1v#-6Y5VGM!HUeWyA6|9r^mf8;EO|C#gb|C>*A#2yALD(k?sS-`&k z=a%||oe#2kA<#CEcW}l+Rri(uOv(_=%s@j@D$f4bw4Mbv56gXwfCK4BB@I?YfJ0K@en6)S_r%v=J(Tphe&=5(ztrDFv1UMKqA+6s9wy zd0=LogHJ;w9mG@;dg0xCd_3>>zwiq{{KcYQfUecB4czK8xHi2391}Zpf^oivlfQ0G z+XE~qj1gO=p4vt~o};oehJHFvLs!18wE`?DgppX~H86}iB~YgYg$Y4%Wd!FX`7`G` zz`R_ixHyP1DbQv-`vi&yDfx%cBR=eh$nOA0y`!=*isq$ivtH`b0M*@bY&p@cnbs$m zH3c(shI_vck X1&GIB!?+o$00000NkvXXu0mjf0y|*? literal 0 HcmV?d00001 diff --git a/assets/img/flags/DE.png b/assets/img/flags/DE.png new file mode 100644 index 0000000000000000000000000000000000000000..f2f6175a1fdc47e946ec70019dc983e8c49e8ad2 GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`x}GkMAr_~TfBgS%&#XG5QHzb? zV13bt0wV^WiOuX28Pgj?EGsM|4Ia$w6yjmfbc&ehRVEP&)X3oJ>gTe~DWM4f0vj9N literal 0 HcmV?d00001 diff --git a/assets/img/flags/DJ.png b/assets/img/flags/DJ.png new file mode 100644 index 0000000000000000000000000000000000000000..a08f8e112957b5d1f249d02c6487398e93fd900e GIT binary patch literal 228 zcmVJM`SmG0audvCSB+ck9 e7~srA2?hWcdFx63V|-`;00008US)G41by{^M8qw!T&qr z+W-4K6aQDa=KjAguJeDPvf=+vvt<7xt3|i~>Q@kM;*tKpNyXs*Z3*@NOPxCZYXrIf zzb>s076Zv4t3{4RIQY8f@c#u$TK{iK>i*wmWbNBRF8JJtWQm9+oQG&Baw5fuxd5WiNS`hUH=&i{>aK<_kn6CVo$ flaK~JGf{#8^8{edKSdC300000NkvXXu0mjfNU5T3 literal 0 HcmV?d00001 diff --git a/assets/img/flags/DO.png b/assets/img/flags/DO.png new file mode 100644 index 0000000000000000000000000000000000000000..892e2e2a6225f4a01512d935a77600939e816c74 GIT binary patch literal 142 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`p`I>|Ar`&KKmPx>XI7n|bhdz5 zqGa;F#mOZFegVA9{OZgGAONSUz(G_l`fU|42;|6;aiTtCnP22WQ%mvv4FO#lOz00PF_vl-T^wcQpJzTU+PGzvVFQ+{YQZ(LmD|8o{(SKG|G!_q@f)C& zX$jU0!j`e-V6pcbcl>|RKlA^Cj8d?Ue?NZWHo!L83~m5NnGIO%_v^R+&x>e+@yn_6 z{{R2`moyiA*tYlo^PXw{@5Sd5b^!=a(bvHp2ETt34n*Ms>;J1%v@l}f%jxs~U(a7b zTsmN_u=yY5WBk9>Ob;A*ujelN|L@Nq;u8|dDRJO46D1e`)IQm=yx>ZX00000NkvXX Hu0mjfA|IdV literal 0 HcmV?d00001 diff --git a/assets/img/flags/EC.png b/assets/img/flags/EC.png new file mode 100644 index 0000000000000000000000000000000000000000..57410880428222be223b9da784697dcd80376b1e GIT binary patch literal 264 zcmV+j0r&oiP)K@m#z4p-sSlJ{dsoW1~B_A`j3o- zLwf(uEcg5$KfUdL*v!iR(@K5*i-mOlM^}f_0B-*&|F`yA|IbKH|DTbR`hP>O<$o^! zsl*$=?6>g0QCj`~`W%P<XI7n|bhd!` z9OtUP>g@bV%>N!U)c>#f|Nr&v?3hbGcz^u(^W(qxb@4y-zYBimDxPhazI|G=sr_D)r~jLS&;GBkt#_WqxPRaNfB*lTZ2X`9 xF8|;EuWzUR|Nrmr!CVG`KVZu=4l?I6FxcA44$rjF6*2UngEfIUhx0` literal 0 HcmV?d00001 diff --git a/assets/img/flags/EH.png b/assets/img/flags/EH.png new file mode 100644 index 0000000000000000000000000000000000000000..a5b3b1cc270ade890b972d0ccc7ad84bbc7fffa0 GIT binary patch literal 248 zcmVec_36&3$q zl#l=$gx7$lb#-tn-f!Ia|12}}{~xz+gAILBQGx6YOaqWL!@$Q~yZ%2eEBpWB?p<^# yR0GoOfuY+#JVr5qRLv*`kfRyJfPv3Ulwbg6g>YwJ_%E;k0000|u>=~a6z)1!Bdo=b&F2g7fCetDkvg91P`LPO!IKx!tq8xRoM^{D8uFkiN0xs z*KRO={D`smlOd38<;r_BUj01O2#ex219gK%+ENK$k|+W@_j>3n#w}PX*O(=4Xy@_L z>p{xu#nH?erVf)e!CHY<(OP*2i5@?~t47VAFbTAp^WW60OYr}nsS!Q^CWVoXAtxhE P00000NkvXXu0mjfU7NY7 literal 0 HcmV?d00001 diff --git a/assets/img/flags/ES.png b/assets/img/flags/ES.png new file mode 100644 index 0000000000000000000000000000000000000000..b89db6856c804333418c29409f06661018cdd0e4 GIT binary patch literal 221 zcmV<303!d1P) XGpLVs;<&I$00000NkvXXu0mjf$B$$G literal 0 HcmV?d00001 diff --git a/assets/img/flags/ET.png b/assets/img/flags/ET.png new file mode 100644 index 0000000000000000000000000000000000000000..aa14723529eae8af7a3ae84590d9ecb280becd48 GIT binary patch literal 420 zcmV;V0bBlwP)D+l?GF&ksQd`FW5 z8s$fdlyLAbI6Zy$EhT(QD2LbW`F`Bq0uaBM_!B@h<@1wi0~Rx4XbdrTh?tfUy`6~G zl5YY%9YimM(XHH*;*c>q6~OjP^kNXvQS&wMa+gSC^C1(7wo9UZ~Z4A&+Q~<(?z*=hL&JImc2fj^(033$j&;cesfjTn}W5o?dc+Rll$pCQ(t zNS&2&mzD6o{St`mptz9cwgHl+LZn;0j5M@xT9n1dP80a<{_Yq46?g?vA)R(Wkb(~Y O0000Ht% zQ~wLtOu#S%y8%F(IW4CAFRKjx&to;|znpXDe+}=(|LH)376W`dibdfyh`uN(Ul#ePAbN7DfH%0;K~i0ZYA66M;GzJYD@<);T3K0RRdX B9OVE2 literal 0 HcmV?d00001 diff --git a/assets/img/flags/FJ.png b/assets/img/flags/FJ.png new file mode 100644 index 0000000000000000000000000000000000000000..1cb520c5967502e38261aafbf608ace089f334ae GIT binary patch literal 387 zcmV-}0et?6P)NklRrrV$MiVT5f$SQs=&T4+b51}WiC5Dg6_ z*-Z!K6cl~}`R6&H9fwk|p%)(B3-9yey=nkWE1~%tKoqgLQ;4?`SglIkJ1ZcM8x#2- zqXx*y@?Y2+wRs_Sg^l$n{JUIM^U49x-`V zP_4l4XL#wf4sd(V71SJiL|j*54mTxt$bka(VD;f|nvg}K8kbjf1i3?a$@ z5bN*XzyHAiXaF(*DKv^%^}lYz_5U6dPW`v;KK)-mc>|gOAWbkVC-)zj4^n7bu;c&o zL)ZR4fA;eKvLko?Ta@lWH2~&c6fdk;@xQl!FIb^=!iN9uT}%IGOqldPw0GKn!;Fn+ zE|@<3zhB`F6a%We_QM0wzhu|{$txH9UokcEf6ub1|KnQ@qZ)uOpg>sKan}Et%U1kf zHlyYLteO4)HGCG3WWTs z=l>l~xU=K4Vwa2`vqP8s-v1x}Uws~*V7AG^gGZ%Z$FG)o!d%WGGmpaS``rIam)R+t x;|}3;k(gKdN76#dQR2w5Tqmg?XIwbh7*@^~+QDRO!V7d5gQu&X%Q~loCIA};PqP33 literal 0 HcmV?d00001 diff --git a/assets/img/flags/FO.png b/assets/img/flags/FO.png new file mode 100644 index 0000000000000000000000000000000000000000..4a49e30cb9e243c85d6f69790b9fdcae7c3ce451 GIT binary patch literal 122 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`j-D=#Ar_~TfBgS%&#XG5F^cT~ zqw~d$|KD#dJt%QRje)J3QK9vO#7%}8i97}$=9M$5*aF_PCJOXkF-Uk|EPPXfA$|tS Uq8{r=XP{vWp00i_>zopr09C;ztN;K2 literal 0 HcmV?d00001 diff --git a/assets/img/flags/FR.png b/assets/img/flags/FR.png new file mode 100644 index 0000000000000000000000000000000000000000..0706dcc0569404b481255d721df7348c5e0f5bc0 GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`+MX_sAr_~TfBgS%&#XG5QH#x> xMek>S-^GK~?!i1ZSqD}yHlJx|Jnu6lfuYLt0%J<%y&j-822WQ%mvv4FO#nZiAFTiY literal 0 HcmV?d00001 diff --git a/assets/img/flags/GA.png b/assets/img/flags/GA.png new file mode 100644 index 0000000000000000000000000000000000000000..38899c4a415cc600d2815904493aa18ed998307e GIT binary patch literal 108 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`#-1*YAr_~TfBgS%&#XG5QH#ys zjQ0Tl48S|SR&RSpuR)-25Nl)aY67snd#Q(;dxedLM*V&!W&kKI zZCKy!82HEt&kP^|CY1T|6ixX|NlHD{{M!J z(tp;9Si%OtFjHmh|DzTf|KIdW|9?A0`u{cu9pYR7!ko2`F!;Vt0_^{1<#J$GfW$y@ zcnwf*5B`7YuoBK#0Lg*WU^M^~w;#?DP6r@0SknPTDRJO46D1e`6DW~8yab3v00000 LNkvXXu0mjfKkSPS literal 0 HcmV?d00001 diff --git a/assets/img/flags/GE.png b/assets/img/flags/GE.png new file mode 100644 index 0000000000000000000000000000000000000000..7aff2749cee4a3d0b195a504cd39ca483ca2c1e7 GIT binary patch literal 122 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`j-D=#Ar_~TfBgS%&#XG5F^bKB zfw_tEd9twcW5Wd+Zeb1G{Q`@Zac^MEVM{RVV?3IN>FVdQ&MBb@0Ky0)82|tP literal 0 HcmV?d00001 diff --git a/assets/img/flags/GG.png b/assets/img/flags/GG.png new file mode 100644 index 0000000000000000000000000000000000000000..c0c3a78fd0ce98aaf78b6930d9b6997df06329ae GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`&YmugAr_~TfBgS%&#XG5F^Vmr zLA+^~h6MY9jg0p1yBZDO@;a33@hD3-@FtrwEaMbc%V3Dqllb8vBf^>az*zXE1VimY W*=PILEL8>?$KdJe=d#Wzp$P!gFC)zW literal 0 HcmV?d00001 diff --git a/assets/img/flags/GH.png b/assets/img/flags/GH.png new file mode 100644 index 0000000000000000000000000000000000000000..e9b79a6dde792af7fbee53bd8137b057f46dab94 GIT binary patch literal 162 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`nVv3=Ar_~TfBgS%&#XG5QHyPk zaL8|eJ~zKJV9>D&6Clvxm(f<@SGdc77#hHap|MCdSJ7jmw(ft(y;Fatbub38L1IK3 zaHq8D|3oeQ|8wMXL z7=zd#F`^CFXKweuk%9exrv&@|ZYg#!8zhF)fLjba|FPjG?!5n>xMG(BVS)xccIN&6 v*q)a%10EVv;DWbZKm+uNFknz*CbBUAw3Fq+U!B$O00000NkvXXu0mjf5-Msd literal 0 HcmV?d00001 diff --git a/assets/img/flags/GL.png b/assets/img/flags/GL.png new file mode 100644 index 0000000000000000000000000000000000000000..6b995ff172146db584cfe4514e9d3e1a743477ed GIT binary patch literal 196 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`-JULvAr_~TfBpY&&#XG5v(bsc z_<{XQiOa0zopr09}P1 A!Ts}{@(o}pKKGxc^w59PDYE1n0KzKmSO0(b z{^vgoeE<6V|A!Z6{(pLT?*FGZSN{L|`|Cd%Ks5k_ou=RZ|L5;Nu5BpFaQp{rh+P27tog?N>w?eERh1|Gj(n{@=NC=l`>3&wwt%;|1Ql!~YNH znf!mOC_yxa8^Bz2=KnHtPm(mF7|`sJO0s4Y0|q`bQGx+XI7n|bhdz5 zLPF|avjNAI>+k>bKNdd3!kdv{(IBo|-5A4Q$dIDVaX5zIut;@dhIoKXhe$i)Mzc%j oSX&;=POw_gCeCJI9N5ITHu~7*kKMa%f#x%Ky85}Sb4q9e03UxXdH?_b literal 0 HcmV?d00001 diff --git a/assets/img/flags/GS.png b/assets/img/flags/GS.png new file mode 100644 index 0000000000000000000000000000000000000000..55392f92f223c800af338a7e930e3bd4977e270a GIT binary patch literal 455 zcmV;&0XY7NP)nnp%9Ki8sh?JBj5zQqvmu=58dt63t%O#AG*?PwclMbjG+$GBt zps7kGSMp=*+(H-aDgmfP@h}+B=0eQ{B@Ks2b*GHF<}g}r$gW;TCtoFi_<-0M-DK#+Hq)$ literal 0 HcmV?d00001 diff --git a/assets/img/flags/GU.png b/assets/img/flags/GU.png new file mode 100644 index 0000000000000000000000000000000000000000..31e9cc578921be803e03c114345b1ab095799175 GIT binary patch literal 228 zcmVvAjNa?|KgLY|CgLx z^FP^ZGU)~^>Dv6i>f+Y_byxTOU);HobOW|7IsE^?<;VZ`U%LN))53kE8_-z3;{WtB zcmIbhTk*fDpqq39qMh3Q7p42h? eIPjT?5)1%t_>9~}OsTm50000-;<5^KR*xS|HbWm z|NmUm|Nr-S+yDRHZ-Nbb^7YOCmJ7@OYpqMiZU7830Hr|q&qXmf#sV-6I0AHmHyhu7 z9xlfJ3tFKrc-Hv;-?z(f7j#`({oi0iHl_g!c;&!hV4=?e41=Wq|2`iEYx?l>%l|ny zcl>wSUV%FnGB_czP*)xakA;m7kN*$c+e|nX3|Kh9>EQ6;!~YLFzw|%t=%oKbOT&my n2cAK`|4R;1kP-(zGf{#8p!$>D!H(NL00000NkvXXu0mjf9XP_y literal 0 HcmV?d00001 diff --git a/assets/img/flags/HK.png b/assets/img/flags/HK.png new file mode 100644 index 0000000000000000000000000000000000000000..89c38aa1a59b5ef95c22404360c5473efbe78c9e GIT binary patch literal 418 zcmV;T0bTxyP)S&8HnEe#F1v^7MHVMEXmQd}{wLuh6K!s;SOzrJz+>7}=_S_JJjC56o5H_3;cQ)}nW(MhNC=MX32ANwwve7W)a}Ol%G2#c?P^2Uj3VXl5LtwZa z{6J$7fKiL^!Zeh)h!66hPV+iH1=)83ac>L3aSH<0R`7#O#Vi<9|1Xmx>LJN1(G^H^VE@>^D=BkjRf5)aO}aI!JfnEdKF^>)WCpk)l6u6{1-oD!M< DO@Kbn literal 0 HcmV?d00001 diff --git a/assets/img/flags/HR.png b/assets/img/flags/HR.png new file mode 100644 index 0000000000000000000000000000000000000000..6f845d5dd390abde214332c8c544f680f0f45d2b GIT binary patch literal 391 zcmV;20eJq2P)pLA!aBTbl_f|`#cvM86c}AHGTdD9^`Vq5>_Y)wY78B(#?rs zh$Ewc9n~Pg4mDw=lH%R6z_ES+m9USwu7}~7QM6m*%q?~?VOir;r>_j$H8!!ky2sU+ znd^}WZj6%zr_Ee0tni>vlm{r9d^$}`-3j-1WVQv~HyjX&@Tk#{D|99E6Ud1o?|}eO zlZlkmNy_CSIyEJ|eRdUrd^StU?IvNfkrV_e3Ag`Wo1 lI;DHR0@dd4fLikZzz2$M)CfJO*8l(j002ovPDHLkV1kZ=uRs6* literal 0 HcmV?d00001 diff --git a/assets/img/flags/HT.png b/assets/img/flags/HT.png new file mode 100644 index 0000000000000000000000000000000000000000..da4dc3b117dd76e8adac30aa68e25b3c7f0564a5 GIT binary patch literal 206 zcmV;<05SiGP)p&YbT5%T}JoZ@?u1lmE#0^Y<_R`<7Jw&x(}(pB*mwf7;5X|6hN6`;V>; zrvVS&Kl{J@?3(|pE^Pth6=&D}fAs!2@dgl!2ay2-pP4AZ08Sf$zUX^p?*IS*07*qo IM6N<$f?pY8p8x;= literal 0 HcmV?d00001 diff --git a/assets/img/flags/HU.png b/assets/img/flags/HU.png new file mode 100644 index 0000000000000000000000000000000000000000..98de28af8ecc6818d5654013bdbc7ea15d3b4803 GIT binary patch literal 104 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%``kpS1Ar_~TfBgS%&#XG5QHyPk zaL8|eJ~=x4Z6N_?=u0lGI+ZBxvX literal 0 HcmV?d00001 diff --git a/assets/img/flags/IC.png b/assets/img/flags/IC.png new file mode 100644 index 0000000000000000000000000000000000000000..500d9dbe2b0a944e4a05be8ca8b530b1eb74e309 GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`b)GJcAr_~TfBgS%&#XG5F^X*g z!?&KP5g-2>^Q@>SkPg`U=EMK5($oINZ?Aj#|J2fU5A%eVP4E8iT`T(k_ie3z>tk-Y z&tiP~+3b(~u6=LoPv4SG;7OW(?ykcg#@pL;f8F0$^7nt6>Ggl+YxaHHFMHzNw$h6M j-K@K$4L;25%;8}W<^S^eq;-%7&?O9>u6{1-oD!MAr`&KKmPx>XI7n|bhd!` w9OtUP>g@ee4!+F)CiA=TE{KYg2w-M-dHSR3tVKt=ftnaRUHx3vIVCg!0K3#3D*ylh literal 0 HcmV?d00001 diff --git a/assets/img/flags/IE.png b/assets/img/flags/IE.png new file mode 100644 index 0000000000000000000000000000000000000000..105c26b8843cdb91675d89fa150e5da98b18ec26 GIT binary patch literal 99 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`TAnVBAr_~TfBgS%&#XG5QH#ys x%%?y7eTELI%xZF`30E2prXDzuuI6RHFmuBBm=>;Wmw~z%JYD@<);T3K0RRfP9<2ZX literal 0 HcmV?d00001 diff --git a/assets/img/flags/IL.png b/assets/img/flags/IL.png new file mode 100644 index 0000000000000000000000000000000000000000..9ad54c5d2194655569a33ebb4cf09e6e8b1b0b88 GIT binary patch literal 180 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`)t)YnAr_~TfBgS%&#XG5F^X*h zw*s3%#G4=fe9PuB?zpr4|C`(YtIzLIe8XCD@qYckzxgwYpV!A6Ry9~RzgAkuZ40Bq zgv-Tm{~!GP{-6B5AOGsZzMEc(W!PjYmr-5wc|= z{C@#74U7N3khA^&NJQoTp`6tJ z=gr;zzu?!wZNMuYt^c?5t^S`63jBXh#PI)v1H1n}NYDL$SIy!7)u#Uc&$-kHhrtV= z-(NDx|9>8s_5aSzJ^vqEJO2N|?v4MiC1?GA#i~L?AOgMdK+5R<`NQk~-$~5>|IEne z|DE{k|M!*5z;c9L01WMCd|LmnC1(D=+|>8~o~Y{om#oSV%|b*(Bg~LD%*y}IN2UEg z86NciCAT{9=>Q&}KpU^Cn*VA_+A6Za#()sEnM;Lao1XclIyr8X_#JK(0okkQZT)~qJ4X1mS6_u%;Mf2_00_*r>89J?vj?4 zm_@d6SsIBS5=x2s_LlDboypb~Zdiu|IU_)m38X~#@&ZMH z=%oZUu4|T$k0smOMHrjE+OQQ2Z2n8O86y}e80ibtIh9L);fihm0000gTe~DWM4fZ+j-6 literal 0 HcmV?d00001 diff --git a/assets/img/flags/IT.png b/assets/img/flags/IT.png new file mode 100644 index 0000000000000000000000000000000000000000..ce11f1f8260ffaecba9badf044c914c532680d56 GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`+MX_sAr_~TfBgS%&#XG5QH#yM wr|5To-^GK8xy3v-SqD}yHlJx|Jnu6lf#GZBZY>q2=^$+kp00i_>zopr07VcVeEP);IdA#{W;P zgZ}@z`w*-FW&jBMe(>b~Q@haruXF3+mY^5_1D_YK1v~HC&Vyi^LHOJLV_>mQ3s(O} z1DFPYz|Zqn|AWHief!k^A9`l}e{A6K|Hs+O=*n;#00MuWJpcbl)$ad8C7b`h?>xY! z1h)adZru6*#Mt})yV{=r@0urqUGVeLb>a;8vTi%rzh5@)f{T6Kwjb=8&nq_JG2qYN z|8J|i!8U_jfZ-}&%sjCO`2VK3860CM2K;&X`u}sE#Q!g&a{vGR@)ehV!E&#XO2KIc xly?3jr$kU_6AM00UHJczmNSYM1~@ZOf&pq0O^WPutcd^s002ovPDHLkV1lg()GGi0 literal 0 HcmV?d00001 diff --git a/assets/img/flags/JM.png b/assets/img/flags/JM.png new file mode 100644 index 0000000000000000000000000000000000000000..378f70d08914e08e613d73bba69e2b276ba8a9c1 GIT binary patch literal 392 zcmV;30eAk1P)Cm@c;carvJH%Wd9p(S_ zH+IdFH;Ka4ZJ32*01*5yN@4i_?FK~h@Dc9+b`#X$8g2SD{_i`)1J_)g!vNBRY5)j3 zSTOv5dIF;8;YHT}5wn#32hULcfA>5aSnT;}hW~DM5Vcqh0AX1vhW|&_Kn(eEgYo}Y zAO^8dZDjbbAcIhc(*O|W;bQoIb_*h0uI^&^&(Djh2B!hyq747{Er)3Qc9Ri|L2Qt# zK(51UfVDBh|3}9lnx9-^`5!w+@qhGeMR0h4#6WU(W;hJUiD&r#`6@*7@nc;7ohPZm zW5IEv`v0Rxxgo9s##DYHngI)Y5&oUOO&DuB0Ex}rDhyY*WCD@_Ac0TU8UL3ql_8iC m3l__QQ!PqL9Qe#c2?hXqqh=9-!FU1y0000{AXum1o9Y2!f*r5399{H!>90Hftihb13>tQfXaVAHa_wVfMH_2 zgkr!28H4}VEWE%ph!0}_xP9+G0e~>zih8#S92Zsp?-yi3P$O0Y)+uWJSIV>` mS~H3P#5x(L0Rx|zD8T@Oqgwym1gAy-0000o})EN-P4nJ z1Gu=j{^wa){l5|#`u}Ep{Qpu@Q~WOYj|FUYaQJ^CCg%U`w6y;l^!5M${rwx8GKc}Y zffO=c3&dVPe8I@*|5Z!N|A9dEGBh>2kzIf;F&l{Wf%q!W-*>{p|62js6G<@udCW`v0xe)c+f__5T0)@e_9#Fr)!P6$vwEu>226QU8A} zH0=NFxWxZ)F)IIAGFkqktHWskM;6C_#x%zNF(ESlbAwgDY>sSB;te1cQ!g(Jr~v?7 Wk4-szV51rU00000u=tg^Hcc$wO{!E@1vamKc>kb zyouESVcq|q%O$`zekzju|D|2@{}-TFKc=sOfKQfw9mn0`|(!9o)b$5c&VsL78wYpriv}+`lnZ`~TV)m<|+3NeASl#DULD alwbhywvkEBYC}-~0000pfi@Lo80;oovn3>>$wgo#~b4 zk<%Pu)2bK_GOt|wDAJ_SHEDYLt{b|yB@e#p-`yh6q;SN+sjI)z;B%-k3&TyOpB)Vo zINpD7Zu%=T@8;cwyZBaR_Wvr)3v}RRX1^T5-TVHrk@DwTo2C?m7L>()&~OrO6R6~y zxGceuVV;V`t#7*)e6)!XprkXp4(-*ndc g0`~j$%XIHBGzUh$(m(L-642=kp00i_>zopr0E;kQGynhq literal 0 HcmV?d00001 diff --git a/assets/img/flags/KI.png b/assets/img/flags/KI.png new file mode 100644 index 0000000000000000000000000000000000000000..e2762a670a12fa986435ee95d60262117de56590 GIT binary patch literal 517 zcmV+g0{Z=lP)E>xzGOh`qYDRtvQC<2v1BEf~?UtsMF2U4k4Eeb7aFh^%lL>v$m{Dpy9 zf;qY|332Xt?{ufYO(+Elg0qM=?w-y$1BVa>7X~d}&iUT+yzl)s*8q&m=u%-_U>8)O z<*vf2xPe{c!7jOpQ{%;{tHAPlv9J8a31u!QswB}?%HjAmz6Xm)_{y=brr$0ukaAUH z`V94{BL?=Mzf#tq_G*V)S{#me_!8s#{5-JHg< zIZL`Yb6lj%Y@B;;;zpTbBr7wzAQcF6^1#nwZ6il5?fhB|K#f>i&B(z)@>E z2ZA}d--*oVf{u|b`etLyy*C+LGnp&oJX5MEHc&8?Wc6s*kXg0W?b z(7QO#BA@7=k1@7lF)F<06Q7kvmn`M=&+jT`NU$=a3!V&b;vZV4I~2t~xXxoScZH)o z3Z>N*-cX)q#3?hn;6-GM@ec`RqhFc)n51`Nn*s5)nT>Bs!G&GrO|B&s6Io3lGrGXI z-HTE8irnTPhVKn(zSBoe(_1BZSDx>6nh~s=|J}dvUkm;K^~k7gUMVy>00000NkvXX Hu0mjfCVB98 literal 0 HcmV?d00001 diff --git a/assets/img/flags/KM.png b/assets/img/flags/KM.png new file mode 100644 index 0000000000000000000000000000000000000000..43d8a75ae7eae80f255461a47a1019fceea30f4b GIT binary patch literal 272 zcmV+r0q_2aP)i=ubFaIC2Ao%~Wdx!rM2_Ob^>}mq@rX1}1-@2pzzrzHx{|isdA>M%9*SCXtxvNtD zPdd>1ALgb0y`7}FV9BZZU>Cp)==Ie3e@;*hCq@Z_mhH9wH(px%KYn4@f4_Re|J&3w z@M^{|0D?{b+cZ`Gw<P)6EuKlf!qJ}2bKOGS;YHaT8!yG z8yg$(2Bsfv8UEMR)%|a8Z-;A^lVrkeK)^cl|1VAm|4#~J2P+2I zJYm9w|C1+A{?EzD`QOou<^QvN0+u}k~^y&e4jjkK6ioD0G{JUstP5;*>U z-7ozAma`(N0iugs{_j2_|9{5}p8rDpjF_6a82_*7=lTC>h3Nmw8Y=(K392CsFkNZ; z|K4ed|4liZ|CyN>Fak?mf%*T{jeP%KwMhOyFQx|8jAFp6V}k#E9azDN(E}^MiS_@x z1A_k_#K^%lVi=$z$Al4BJlvfBTk|>pf8Qni|Av(khGrB4(ETeb#S0FL5A((TUs6=T xsu`yNpwI`!!t+Y$|L29VYs4^M;4>2?7y#3fMtg3Z>Sh1{002ovPDHLkV1gko$cO*{ literal 0 HcmV?d00001 diff --git a/assets/img/flags/KP.png b/assets/img/flags/KP.png new file mode 100644 index 0000000000000000000000000000000000000000..b303f8e7e5687a28854e9e5151ac3510772eeae8 GIT binary patch literal 197 zcmV;$06PDPP)K2M`0^%BucH#vcR1{(o(5|Nk*G`ailHGRA7a2PcpJ zzt3O%|2Zk+|L;qe|9=XLAFLnsU*6=w! literal 0 HcmV?d00001 diff --git a/assets/img/flags/KR.png b/assets/img/flags/KR.png new file mode 100644 index 0000000000000000000000000000000000000000..d21bef98a8ecd038aa65566dacff85b01fdf411a GIT binary patch literal 413 zcmV;O0b>4%P)p*pCJk%FpWC9r#JMCu3(*7Lvm~)jfIIGU18K;Kl|@w z;PrZm9_;hhY~kNlvCMAJ?@;h7Cm4+uXPSz~XI7n|bhdz5 z;tlVAV;|k0=M6T`QRy*Y@G#Wy{Ga~f+`y85}Sb4q9e0FoIUtP)2(RK$<~{kqrRxL52`z z0EqSX@8ADm05kv@z!cj@bpGG7!tlSYXFnDLK$>7!PVPT4AEb~8h@Il<|J#JS{nz&B zz%&5nUlcE_Sn9 kH-M;gK(PUXATv>b0d4V((1i)%WdHyG07*qoM6N<$g6X)a9RL6T literal 0 HcmV?d00001 diff --git a/assets/img/flags/KZ.png b/assets/img/flags/KZ.png new file mode 100644 index 0000000000000000000000000000000000000000..1a0ca4fda91cb0b52b3e8ac051b995fb28dd3fe2 GIT binary patch literal 405 zcmV;G0c!qSa0E@!ng0yy&O`9} z)3^;#-?a9B@80>zOHfg7L(bV1#&od0a=kAW311F`hR9shUicliJR zI0G1iEZ_xtkqMg%^fxd2KXXs`fA$SWf&RVt-)Gyz|0z4_|JUrw{r};J(Elfg<^IQQ zZvpeQH?P9u0#F#NJNI90)4KnW+uQ$}Zdvp{VSDrc@a>)d4S<&9?x^@LykReKVIaD3 zH_#i~{yT1+_1|jC{Qve_X8#ufYd(uD7S;e`Aqy=Q&H}^Y)PJqbtN&YXnfG6A(-vrS zl9UcW_#9N@Io#=hf5ZO&dYe~}pArW?Gf{#8t7o)zj0xsh00000NkvXXu0mjf8$Qw5 literal 0 HcmV?d00001 diff --git a/assets/img/flags/LA.png b/assets/img/flags/LA.png new file mode 100644 index 0000000000000000000000000000000000000000..f78e67f6a3d5e97483d99f91202403ae9a5b191d GIT binary patch literal 175 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Wu7jMAr_~TfBgS%&#XG5QHyPk zaL8|eJ~g=VX15DtLD4@PC61-+%C4;z_uC z_UHfS=i_%U&fot};V$cK{@6z84JDs7FYz+$RN9vF@BfNPJO9tzy7_~&0nep_2h!EN a3>bdZq$zE0IC~xF0tQc4KbLh*2~7Y1`AiJ} literal 0 HcmV?d00001 diff --git a/assets/img/flags/LB.png b/assets/img/flags/LB.png new file mode 100644 index 0000000000000000000000000000000000000000..a9643c34efec418dd4dbe5f9b98e4fbf07d13ebc GIT binary patch literal 213 zcmV;`04o29P)|3BZ#@c%q3LjMOH?)?Ab&oAN)xcBA7|Fl!n|4Xio|F6C|^MAqF zdH?TzenGqeKY%WH@%{b()~jp&_ubg^|Ifd_|G)hHj@y9Ykq!npGf{#8bM5HAVU`BF P00000NkvXXu0mjf=CNzr literal 0 HcmV?d00001 diff --git a/assets/img/flags/LC.png b/assets/img/flags/LC.png new file mode 100644 index 0000000000000000000000000000000000000000..ab5916ba4c2be1c9677d8b36677139df0332f548 GIT binary patch literal 197 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`J)SO(Ar_~TfBgS%&#XG5QHxDw z{_p?uWqd9){CX|mvyD-Do}&imH;6l_eg3VvA#T@VkEXDr<7gy#v_{ zCte!b$t3igmbTz!u&m6A|M#E&xabP0l+XkKIyy;4 literal 0 HcmV?d00001 diff --git a/assets/img/flags/LI.png b/assets/img/flags/LI.png new file mode 100644 index 0000000000000000000000000000000000000000..cf7bbe4950610dd605c44437d0a09ac5be17fb49 GIT binary patch literal 216 zcmV;}04M*6P)|k`)}j!{h!mY8WJ8vhk<4T#75O-2#rTGiUH>Y)&7%%M-4zSVBj+oB^Uq!V^0lT S*_6xx0000aaB6NBXO8URw%r>yaRvcA&)P8qHL zeX8pJC+jNzpKhY`zeiC6EKksY+2)G>R|Jdw-S5x`_iF&U82g`W>PtsH)Yycrl{d7QCN*wsiL;(f> X(=m|Ar`&KKmPx>XI7n|bhdz5 z!o=v`;>Uku*jNnP7z0&{AF>H>3b3V^=`nX7;8DEE`fA?S|8JA4|7-8MGT}K>*P(`p p*9E&8ByPLKI^1Aoo-Ku$RAp9BKo5-&}j^wu6{1-oD!M<59v-* literal 0 HcmV?d00001 diff --git a/assets/img/flags/LT.png b/assets/img/flags/LT.png new file mode 100644 index 0000000000000000000000000000000000000000..f40f2e288250c4e1669dbd099921946a5e3ea770 GIT binary patch literal 108 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`#-1*YAr`&KKmPx>XI7n|bhd!` z-#d|i{(N$NYz8Si|ERO`D>3t}di@W`P-kYdGY)KGoXpyu&wjaZ0Z==Gr>mdKI;Vst E0M1h$hyVZp literal 0 HcmV?d00001 diff --git a/assets/img/flags/LU.png b/assets/img/flags/LU.png new file mode 100644 index 0000000000000000000000000000000000000000..92e72f9d187f45bb85252702aad10a87a42f57da GIT binary patch literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`2A(dCAr`&KKmPx>XI7n|bhdzb zU&f68>g@dL%>O3yvvo7_aLfGxb2Sb!=QA*zKmYV^^KuO(pk4+~S3j3^P6l{lMVl$Fl$K~NO3nS8V5EpZWUjc`CD<{YoIO$Pgg&ebxsLQ0KOj_j{pDw literal 0 HcmV?d00001 diff --git a/assets/img/flags/LY.png b/assets/img/flags/LY.png new file mode 100644 index 0000000000000000000000000000000000000000..4db084539896c890d51af875dc3cea57db2ede69 GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`vproLLo808y==&JSV81i;dGwD zg3LXR%X$3t;ue&1SX3~df4D9?-29;T&6~QnuRnLLiLXv8TW|nXeW-E**95>&T%L<6!%RL+VefY>nro?T}?qNklc4l)?XbKG;L|f1oD7_VYgVHA`K1mLtJ$X?SJ!qEXR1f+>zq2t) zPf1J;9zGamc0QP4)&t=6p;z;-;7x_-Lao3G4~lUw*uMy}s`>pm#A?V`MqI|Ws?`eI zgJ)&%GB7B7gR^r&EEGrIIakZ-1i7$PgwcdSQvzLR(4_`T97%2?wY+JBVh|!_10#)# zNaIFe5v9hIwZAr`&KKmPx>XI7n|bhd!` w9OtUP>g@ee4!+F)CiA=TE{KYg2w-M-dHSR3tVKt=ftnaRUHx3vIVCg!0K3#3D*ylh literal 0 HcmV?d00001 diff --git a/assets/img/flags/MD.png b/assets/img/flags/MD.png new file mode 100644 index 0000000000000000000000000000000000000000..21fd6eca4641bec3992b88a7580dabd2899985f4 GIT binary patch literal 190 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`t)4E9Ar`&KKmPx>XI7n|bhdz5 zLc;97`?34^MhQpzMOgzrD40KZDtYYx^6eY`?%&k@_5bP3Jxinwws$`NKZoPs|I5y- z|MlNKRJz-6E$HI^sg`X2KL_jl(|;=l6iJ79r6_)IvOf7J>m0(KLeTEc5`T$O%klBwTDw4{Nl} z>s*{Qke!4{r-oV-78&@0VWWz?j=8$o;l91e+uaKN-H?H66Qa4rw$E@+&)!}_-bSeX zfbL;R-?PX-&T7Gip0(zYT}EWB2!;BHlBhAbPEtT$+Jt(w^|Noha@Cj5( Vn558+ZYlr(002ovPDHLkV1giWkQ)F1 literal 0 HcmV?d00001 diff --git a/assets/img/flags/MF.png b/assets/img/flags/MF.png new file mode 100644 index 0000000000000000000000000000000000000000..16692f7112bfe502c5cc6f9bfefc2e7a0ad318f5 GIT binary patch literal 161 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`8J;eVAr`&KKmPx>XI7n|boK<> z1a1X3lQVWcoBUSDbujWgd;8zhkY$P36857EIc0=Xyn-gyT z{mlTK~V^ z*pBG}9>*E~yB41X(=ag;w}Su2@&&*e@7MDG|8TJP|Np;#{zCy~7{F|1ww(AsH_Yq* ztrDjHcgvaozh0O5|L@ng|Iq;USP-{s{=Y0u1#Ii{IUfIizPJk4@#NK)|6NPY{x^tS zj@6g^?G5LrptDCN&l_R7k}hK-bzy|S7Az(QoDY}ok=B=gHG{ThSJ;ti9K?(rP9(kU&)#0UtNO zWZMJ-;Kd|Zo3MTm>>N8MP0*(X$qx9i242p9^(l96F2&>C%(;m;X{6OmCd6<=xp$5t z*4_up!{5T|4Y9pvuIrw5ecBBAz`Mns6BL}2Mq15eGAIvOXsckW=q4KIzLMpU>^dh+ zCIh{*w!EMD#@3b#ly+mmdtu_F$$W*s-UD&J-h%+jy*BO}m3F|@hnpBq`6I-C@H6G8 YFC{&mp50DC9smFU07*qoM6N<$f)60A-T(jq literal 0 HcmV?d00001 diff --git a/assets/img/flags/ML.png b/assets/img/flags/ML.png new file mode 100644 index 0000000000000000000000000000000000000000..bd2384186389116aa2285cc87138c313b711c54f GIT binary patch literal 103 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`dY&$hAr_~TfBgS%&#XG5QH!l3 zr|(yP-~Sj9=Y!8#t{Qw;!MK=>$B3ED&N#4%kwbKHW2jmdKI;Vst0Ca^N Aw*UYD literal 0 HcmV?d00001 diff --git a/assets/img/flags/MM.png b/assets/img/flags/MM.png new file mode 100644 index 0000000000000000000000000000000000000000..1bf0d5bb4d7b863f58c24a33f3fc9ffe1f9a3958 GIT binary patch literal 195 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`U7jwEAr_~TfBgS%&#XG5QH$-5 z_1O>eWqjC~*JU^x>}2~>e~aNrZws%%&6Gd#lI5}Q{?vcEdh-AL`dYSR0cn-Cb$rVi zm(BM1r@wFW4|^V+eVaLS+_o?(+$rAq`~U9w?+-{EIkt=~?|?S%>HjZ&6dzymKR>7D vz>nhJNB-Zq^1!q~!lr{q;>Q^mPBw;_QoB`Smj1l}bQOcAtDnm{r-UW|7|>Gh literal 0 HcmV?d00001 diff --git a/assets/img/flags/MN.png b/assets/img/flags/MN.png new file mode 100644 index 0000000000000000000000000000000000000000..67a53355d873f6f0d6426511d7be2509772f5a3c GIT binary patch literal 225 zcmV<703QE|P)*PTbf*woO#04Yig5SID>EK1`4Jr(KycSK1w0I1=%pv?bI zD~10*vXlCMo0kk1Jn@nI|E5{=|ML{_|M%3SNi*QCq|E=1ON9Tw?GgR|X|3@8$L^9O zx4%P)66s*e88RT=x=Z?fzEfGMtEHdjqFkp`3|{#R=a z{;$*K_g|tR`oAO)Yq$CTS8odj%i%YmEaAU#m;Zm!I{*KIP5J*QIg@J65 zTL1qhU4CFSxD6=-@pF`lj^`?oK>MlgnoK6mv0yKi6qfB60t%-(k6;(zzKJzzDs4JZo#AF^aJ(B?lN$^YMe{Q}~j zU^d7#Vauj~)!;UuH2S|vSIPgDy&M0px^(h?*|rt`OMw`~Zr!)>ziM|0SPh;qi2cuB zpZ4E>VgLVv%}f5LteN{i8HfuuFa7VoXu^L1pd3gI{%9am9cE^7XpcqJwsDuQzr8olU+$abJv5{=XFks*_6D1e`F(`hF#{F$700000NkvXX Hu0mjfv47Mw literal 0 HcmV?d00001 diff --git a/assets/img/flags/MP.png b/assets/img/flags/MP.png new file mode 100644 index 0000000000000000000000000000000000000000..b50575402d5d70745024b3dafe65c4faa2759aa1 GIT binary patch literal 548 zcmV+<0^9wGP)B{ z0Ga}4eO2n7JLKXvF8NovF*(bSHN-`KmgSg>M(Qp{ZFPjU&DkPFo+;%m+<{Dx! z&7)Wn?_3NqxI*`64Ufl+Km39{`qB=7US_|!N`1z{75f}VtQAIOnHO!8%k}Ks4a5M9p`sko>_H9qZXTn z)z`ngTe~DWM4fV5(Q; literal 0 HcmV?d00001 diff --git a/assets/img/flags/MR.png b/assets/img/flags/MR.png new file mode 100644 index 0000000000000000000000000000000000000000..6bda861307ecc8981bcd7d28a76226ecfff5df9b GIT binary patch literal 250 zcmV_aHSc-*}BDAU<~8)rW+78K&?<4OsfOw zd)wK;SRE>+U8F_WfHg}5;q2rNc`%0Y_pah4Y`}+o%>O;=R5AP;)vEaa^L{462CQ2u z@c-u_#{ccpB>tO~Yy7vY(D>gwL;U}r!;D}#f(B?6Y5#w_m*xNeBMc}QBnFZrDjKD8 z^!~2~hQ;&UtY8dcgT#nV2NWkHk_{O6%tQ$W0IQ%q;um>kwg3PC07*qoM6N<$f=xtg Ak^lez literal 0 HcmV?d00001 diff --git a/assets/img/flags/MS.png b/assets/img/flags/MS.png new file mode 100644 index 0000000000000000000000000000000000000000..a860c6fe44ddcb6ebc1b93d270e55aceae930c13 GIT binary patch literal 346 zcmV-g0j2(lP)2(RK$<~{kqrRxL52`z z0EqSX@8ADm05kv@fD{_e+WB9uYsr7Lf|mbEJ=&)$V*07w%I%gOym=7SUpE!g{? zrFZFnrwG6Q5=|Zd1r{AdH2~&c6fdk;@xQl!FIb_(++F|KmL30ZTb1@-W%2C)B6Ihl zxnTPA|9*u#Pzb0sWqffA`i$ga7~l07*qoM6N<$f|$vxb^rhX literal 0 HcmV?d00001 diff --git a/assets/img/flags/MT.png b/assets/img/flags/MT.png new file mode 100644 index 0000000000000000000000000000000000000000..93d502bde51ed3a1a0e6892f8e4b70b338b16ca1 GIT binary patch literal 114 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`mYyz-Ar_~TfBgS%&#XG5F^cU0 z%X8N)g-JZjeKNP)_c1D*T|G_rfI&;0-U2U)9w~zZY&cV7P8SorSl8K5By Mp00i_>zopr01Lb%`2YX_ literal 0 HcmV?d00001 diff --git a/assets/img/flags/MU.png b/assets/img/flags/MU.png new file mode 100644 index 0000000000000000000000000000000000000000..6bf5235972bad2a32e00f740bd531b24c8ffb757 GIT binary patch literal 116 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`)}AhoAr_~TfBgS%&#XG5QH$-) z#0kIs`Q%*K46?TVS7+x}V*a=1*?)h&W%C%5CO7?`+{`|ak$L`1je`td?Mo(Z=MT&R P8pGh}>gTe~DWM4fZdE0U literal 0 HcmV?d00001 diff --git a/assets/img/flags/MV.png b/assets/img/flags/MV.png new file mode 100644 index 0000000000000000000000000000000000000000..b87bb2ec063d3cb49fde7c1e95abd3a406a63b0f GIT binary patch literal 201 zcmV;)05<=LP)V!D%lrRz`)2>=ZFTw2+F(b# z0i4Z_|KEQ5_8BTod63VTp<;sHy?t^c1q qa7=0VXmc6Jc$jkFK)RZj0Ym%`YbB9_&#!=PV(@hJb6Mw<&;$V8yG~sI literal 0 HcmV?d00001 diff --git a/assets/img/flags/MX.png b/assets/img/flags/MX.png new file mode 100644 index 0000000000000000000000000000000000000000..8fa79193a92c30d6e36b934617c06d4f43d7c794 GIT binary patch literal 207 zcmV;=05JcFP)ut&ssVCe|=5*|9!jH{Qvs(EeT%u_wWDH zS9kue+qe4v+MSF3KYVrrDE621K>YRnGZ+uiXe7yifzM2oU;xrI%SCALqL%;w002ov JPDHLkV1iL5X|DhP literal 0 HcmV?d00001 diff --git a/assets/img/flags/MY.png b/assets/img/flags/MY.png new file mode 100644 index 0000000000000000000000000000000000000000..a8e39961ddcf090c556e671d0ba6850be28fa61f GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`n>}3|Lo7}w|M>sko>_H9qZXUN zjCFtgdG;-S(ccuvldw7F=l-R-KmJcR`Sj#}TcPZZ;)AP|YyO|l4{Xsc>s`;h``J&$#{YgBd0S!`j2^rylN4cgnB^#O zMC}FB3~?jx%MaS7Tsg9^f^j{!1LL*ZZ@IU9e8M*MziF?aL}SNw)*}gX^c>7*M)b@P kI>~ya_#i{NnwJ5CpVU^_rW)gJpz|3#UHx3vIVCg!0CrzsC;$Ke literal 0 HcmV?d00001 diff --git a/assets/img/flags/MZ.png b/assets/img/flags/MZ.png new file mode 100644 index 0000000000000000000000000000000000000000..0fdc38c772c5d61bb2d826d1b19aade0ed26385c GIT binary patch literal 315 zcmV-B0mS}^P)5DZop-r z<{Lt0|8L3|{lCO-`u{;}>HojqfBYu`Knyq`Z1}%YPW1nlT=xGf1=apv5e9mRSNp#L z69-TQ15S)EKvV61Ns8Y8;-<3ycP88Zzba+`_R>Lqga2LJN`wtq$D;LrWv%)DTN_jV zZ}l_yf05JV|0X`2|H{mq#JS)y(8;%yb^c%G)%$-#S?~YD#ESoazkDS=3}CTvS={LV zn@J-7|2$+M3Zocs!$9Z%*ZrJCYeq5PQM|(cU)PyP(u`ujz-J~(FaVpfk#r1@)B*qi N002ovPDHLkV1oCXn9%?L literal 0 HcmV?d00001 diff --git a/assets/img/flags/NA.png b/assets/img/flags/NA.png new file mode 100644 index 0000000000000000000000000000000000000000..52e2a792c117ef8c4a8fd813844fcdf771d8b4d0 GIT binary patch literal 452 zcmV;#0XzPQP)7G6`MWhu9NzC}l7$cla<^6LE1i#X!H(2SZ# u01?S+$Je%D51&GyZ~9SY1^)jt{f1ZD>yvT<*6v6E0000EB;SQ;Qe1!A^krz-3u&E&;XF4&R(tm zUtb9S-@JX literal 0 HcmV?d00001 diff --git a/assets/img/flags/NE.png b/assets/img/flags/NE.png new file mode 100644 index 0000000000000000000000000000000000000000..841e77fb59f97010c6601cdbf89463b6da01c0fa GIT binary patch literal 153 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`37#&FAr`&KKmPx>XI7n|bhdz5 zj%DLtb#{Jr=6}hI_x0cY|8L>=>;Ghvr~miM#T;A4vY#>kx!A$nUpq3HUb6k!QTpot zW0jl#{WqEX|6f-rzn^iB^p^jVo7*QdPI=I1&cM(TntW0|)P&{D1Vp891LL13v!v{6BDU#{Y~B zb>tba<;H>kx)c2WJIzk`fA8&M(hT_X_s{>VjjjL9r$zo3ZF2mdyte88-+zCJH{k2f zZ~y%l=l@sl_5Ux~;_}~iX7c|xU*8dL!1GTp{)aCs{4d?^`Cqdy@W0ix=>KP)Tq53p z_5*YNi!?d?=WB5I&sJmoU$MjefB%vB#2c{m!q)$#+xq_pFD?I{wyypEj+=-7pM83n t^mMTO*0KNFZXP8)8c9xx1D}~F!2s}aoHbJ(^Rxf}002ovPDHLkV1hf$oRk0n literal 0 HcmV?d00001 diff --git a/assets/img/flags/NG.png b/assets/img/flags/NG.png new file mode 100644 index 0000000000000000000000000000000000000000..25fe78f02337ba82fd21ad188e5685116565535c GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`nw~C>Ar`&KKmPx>XI7n|bhdz5 v;uiD&#mOZN&SzIJE@tCVW7asxoX^0ZFScsFsqv-PKurvuu6{1-oD!MF)fo#eeVpqOJi`?Z3$X#W}wBlS#`7dV% z-a@mZQob|S7lt01Y{Fc$TgkQKp6yXvsr-HQ!6`~tIoF>jsX z(r$SBa?ja9KN;Vh*RRPl6r}ddU@%ln(qC`>qu=_+C*!RznT-{`_3&LwvI9Dj!PC{x JWt~$(697fuQ1So( literal 0 HcmV?d00001 diff --git a/assets/img/flags/NL.png b/assets/img/flags/NL.png new file mode 100644 index 0000000000000000000000000000000000000000..036658e75a8073b92174a8cc45b33e2e197ef18d GIT binary patch literal 104 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%``kpS1Ar_~TfBgS%&#XG5QHyQO zg4D{1d~6oRflZ8? XlQ$aPRWa8E8pq)2>gTe~DWM4fdle>V literal 0 HcmV?d00001 diff --git a/assets/img/flags/NP.png b/assets/img/flags/NP.png new file mode 100644 index 0000000000000000000000000000000000000000..eed654be442c77cb62b7f6a0eb24a482d3952789 GIT binary patch literal 241 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`J3L(+Lo9lefBgS%&#WpjHQBII zR=}ItkXfHQgt$3#@9j~)3iJbmr>mdKI;Vst0N)x|6#xJL literal 0 HcmV?d00001 diff --git a/assets/img/flags/NR.png b/assets/img/flags/NR.png new file mode 100644 index 0000000000000000000000000000000000000000..4b2d0806d6f76b1e8fbf64fa325fa86848b398b1 GIT binary patch literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`#hxyXAr`&KKmPx>XI7n|bhdz5 zV$19QlbhXn9gH6QH{AI@B{4^DLfM!l5NeI~zQRb1Wozxdj`zx9v*WgGB5nw-)w z$NT>O$*m>KdE Wg0`=mSv(JDJ%gvKpUXO@geCw>)Q%BfHZ>?pTE5O z|Nre$U;{vWupz`4@c-Y}|9^kp`+wm0mH!q_8UObkzw!V7-}nE)fG7j9y#N2d$oYR~ zY{>teYZU+QN(lP@|F{6yJ46`(cIt`M|4&^y|Nk%03&$=#`2Thg{`CL<_#ki?RCnzsJ`nLL8a04O0|q`bQGfy8obw0f`T9Zt0000a%pGdybMalH27?}5ZOYz-0h z|Ns5vXLDelz!<2h~X# y)nE%Lf2(dM@KIALhV4PmvBvL*6i)bvBrqg!HdiO@JN5wRGzL#sKbLh*2~7YT=1zzJ literal 0 HcmV?d00001 diff --git a/assets/img/flags/OM.png b/assets/img/flags/OM.png new file mode 100644 index 0000000000000000000000000000000000000000..b2a16c0300cc5e93e2e30854a06d068d2fedcae1 GIT binary patch literal 198 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`y`CR*@Tf4|kmE!pw5? z-+m9z|Nl$xS}Dx^_J3U$TPDzqiGTiIIQH_qsQ=0$) literal 0 HcmV?d00001 diff --git a/assets/img/flags/PA.png b/assets/img/flags/PA.png new file mode 100644 index 0000000000000000000000000000000000000000..fc0a34a37d4ce4aee782286e5b84d8efb5b2c717 GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`rJgR1Ar`&KKmPx>XI7n|boK<> z1D5G5J_Si@*m8D$>8oLWCR6!yzT)2-|M!~PO-Mg*c)R_-_}Xu8|F8Dnf8sk+WFkY6 znVt{BgtPAr`&KKmPx>XI7n|bhdyw vkJIz7`|%eH?Xv=z-FPK@*aDi^?=dh$`tD1XUa`&(sENVT)z4*}Q$iB}nxh+B literal 0 HcmV?d00001 diff --git a/assets/img/flags/PF.png b/assets/img/flags/PF.png new file mode 100644 index 0000000000000000000000000000000000000000..c932709619301fe143eccbaa57d944ad957667d7 GIT binary patch literal 217 zcmV;~04D#5P)Nkl z|3d=${@2YqL%ae1{{8kL{-j6 T(Gb`p00000NkvXXu0mjf*%NAE literal 0 HcmV?d00001 diff --git a/assets/img/flags/PG.png b/assets/img/flags/PG.png new file mode 100644 index 0000000000000000000000000000000000000000..68b758df2ed134fb7b782b569a82f76114161f16 GIT binary patch literal 444 zcmV;t0YmCu>kX_eZ1##Vl)Lq+M{~S6kW*q_{@F3X1TadTT_67O~9qR)G9lF}7E_F>) zO4C(@=mP{11zq|z4k*RiAqpCJhL@L_XXbr}aRG4s2G^emP6ZWq*hD*|<9Z%p9jyR? z|C*l(B##&S0hvl}Lxw|eW)_E>j1iG^$ij!6INmO!c()_KID*$R2_0n#t>7F%sZ>H~vqIx- zpMWMgWt`P9SX1$Q62sGxjQgzwx|tO1$4tQUJQPL2RFui(AjTx*ynYk6VBu1e$+QK)lFeyXVbubL$5KIu=4EES4?!pGT1c#tpE~8ql&fWZ(jRhX( mqUIF*ghj@OzxxaSD)<0>rYyDg6(nJV3%>9B|M~5{|4FT9{&U)|Cc}W=Z~y=Q_W>mE z|I*!W|LrpN{b#aVNrC~$;P)*c@$>(xy^sGZ2X7(4fWP1W|9dm%|KGW)e%>ba>i;e%E<(2Zplw*UYD literal 0 HcmV?d00001 diff --git a/assets/img/flags/PK.png b/assets/img/flags/PK.png new file mode 100644 index 0000000000000000000000000000000000000000..014af06529360eff84aab14c68af58f1e1ccaca0 GIT binary patch literal 306 zcmV-20nPr2P)DHa=Op2dI(3)78&qol`;+0AeH`2LJ#7 literal 0 HcmV?d00001 diff --git a/assets/img/flags/PN.png b/assets/img/flags/PN.png new file mode 100644 index 0000000000000000000000000000000000000000..c046e9bcc7060fa6591c7b78b5c52d04191ed78f GIT binary patch literal 423 zcmV;Y0a*TtP)-R1Ou{I?>Y%Yu5gmf0PMtys zx^(FxUHk$4i++W?HG;JgrC^5!Ugm?DhnZo90zmZ>>TiHX1JPm~jn_9o5L$I0`2;df zId6Wqz z9fUT_!&rL6=|dVLJNHJ1oT4iHR*5fOQacQ!Do4d}T=4A=_20W!xGbe?U@ zXUxdl>R<``FlgTe#a#CWkf@jf#@%c1#-}jqNU;;?fUYm`AAF`h@(z*bfR@4C RDy#qi002ovPDHLkV1j85tkM7g literal 0 HcmV?d00001 diff --git a/assets/img/flags/PR.png b/assets/img/flags/PR.png new file mode 100644 index 0000000000000000000000000000000000000000..7d54f19aa93af2bbbe31fbb32af1ce38d4354748 GIT binary patch literal 216 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`^F3W0Lo9lefBgS%_>1+Y> zoh{k_-isRksrL)$RpBvjIGA*_-tqFz|JA8~Pnt8dIj`#bSO4|j@p>i3WsC|suQvYs zzl*uw)0`p2dXuBxt*sS@*^V|`Iw^j@t?jyuv!Tn$j8FfCmR1~Md*PP<|9xP=hy8Q@ ztL;d0pIh}Pwn2_fyUboiUx4{^{{(UF1tk|0#a-g86rO}Pu>~}--(z6-6THzX(@D5s6+T(+ z*tYs%eVbPFB7-)@B^k#I8bZ!$uuWJjd9k%IM+6v@w7w-S)qJvC03WHogB>qSXFhw9x$jhFAj_GwuHO z&(Qt9CCc*u=1}|p_Z)2hpGr0Pe=Sy%I0H0FP5$3nrttqmX8QjtXRrOgb?U;F@9h#SIza3<%$F978h<6YYhk*X1xY-@c+vyaAQF=Kue8 zhYPOx%WYn8ppp~|(VGtapL($F|AYh0|06dZ8Gv*^vH=61nJB>kvXJ3)YEq6C00000 LNkvXXu0mjfuA5(L literal 0 HcmV?d00001 diff --git a/assets/img/flags/PY.png b/assets/img/flags/PY.png new file mode 100644 index 0000000000000000000000000000000000000000..c289b6cf72d001867d7b204aec97e1248433c181 GIT binary patch literal 197 zcmV;$06PDPP)kWUkwm1#B@t00000NkvXXu0mjf8R}Q9 literal 0 HcmV?d00001 diff --git a/assets/img/flags/QA.png b/assets/img/flags/QA.png new file mode 100644 index 0000000000000000000000000000000000000000..95c7485d574e4519866d3988985defbb09e76aa3 GIT binary patch literal 190 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`t)4E9Ar_~TfBgS%&#XG5F^a9> z;PvnF2Y;M;m;7J-SK7bx!drjED`a*HCrdD}{r~rWfAOC~|4%krPLVWFwU}PR{BGV3 zjxtBRoosjPs^0xqnIL}Tgvo$i;|Ct^vz;^uH|M?;loGT>)#CoS3VY>N}VUE!K q+CTN8Q-uC*@-RwxU@Ux7f?>b=&U?!$#VvsjV(@hJb6Mw<&;$UkZ%_&V literal 0 HcmV?d00001 diff --git a/assets/img/flags/RE.png b/assets/img/flags/RE.png new file mode 100644 index 0000000000000000000000000000000000000000..2ff851c8460ed073cac981cf2ba0bfe74ee78f3f GIT binary patch literal 443 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbMflK`I(S0Mf84{O!z*(%Zhjnn=c zB>!(*IQ!@ok3DCDV_Hucrv5idgNj$pUSgN`(I^$9rhfiBpkl-H{~@7Qm+BUNW99zO zz_3jv`Wq_`kdZTC`{G@Ng;O_JWc>iCPx+rSal2#T>&n^7YUVC-Eq)F(A*S`Te)50) zt#a0~yh)o=x(-G(pZBT0 zo0N2Dhi*ht(m}n{|K^F`YV8-@70?D+3UnvXJ;7mDH><^eXW_C=`qJPq@4kS>|Ns9b zADG+*`ctMP$S;^dSP2B`9^40miC_TY0mUtz&-w>cm*wf=7$Ong+P9GJP=W+=_2b*; zd4$-zzSnEL;W;QEW^sh$rh)Z!@7X3dnHpFYbw>5hvenl9Y* zpDS*aX8!rlcW%7g8hMv9!7HPxP^YRw#d8@Cm-~b7(QBigxNJVj9?I@8Lx?4063|u# MPgg&ebxsLQ0H#F7MgRZ+ literal 0 HcmV?d00001 diff --git a/assets/img/flags/RO.png b/assets/img/flags/RO.png new file mode 100644 index 0000000000000000000000000000000000000000..3d9c2a3ec39cf38179f44546d3d73470bdd8ad0d GIT binary patch literal 103 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`dY&$hAr_~TfBgS%&#XG5QH#wW zX7Cq3HdL~hEr6G`+Z6+i0{FxdD8Itp+sM-GLs{ra`@O1TaS?83{1OTPA BAYuRj literal 0 HcmV?d00001 diff --git a/assets/img/flags/RS.png b/assets/img/flags/RS.png new file mode 100644 index 0000000000000000000000000000000000000000..d95bcdfc23a91f6d7f601dc07b4a9e4f8de3da03 GIT binary patch literal 331 zcmV-R0kr;!P)l#d(lszzsFM{}&Qe|6i?>`G2)h_Wvad1HuNV*tPwK;Ul3L|4(Ja{y&yd^#4S9 z^8Z8Wx&M*nkug>S?k`^X|8ZH_{|6g3{=ZjJ^8dl=^`sea@A%pOFHfER|FpUN|C8Q{ z{~tbmMWPEH=-aFy{{ln)-GvGvW#Q%HQzW<-~O!@z?DF6SZvOvNH z{6_*Gk8b^cJ3#RNhgTo|Kbz6?|6a1ne>?!w01)`ReaHW2_09iZclZAP{rn~A29N-# dlotln007YC%v~N~bXWiY002ovPDHLkV1fjNu@3+M literal 0 HcmV?d00001 diff --git a/assets/img/flags/RU.png b/assets/img/flags/RU.png new file mode 100644 index 0000000000000000000000000000000000000000..a4318e7d5e357cb21f4c95c5e8d4678c9d753e10 GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`nw~C>Ar_~TfBgS%&#XG5F^X*h wcUv!`!Xz1+4xS(W`#29DZ#Xz%=80nrX|fhW=gV?@fm;{av!VAwl8at|LF@%lOzbiwNcXpWMto gk&$`+OpSvKKVHtNkWV}Q9q14SPgg&ebxsLQ0Dy!^0{{R3 literal 0 HcmV?d00001 diff --git a/assets/img/flags/SA.png b/assets/img/flags/SA.png new file mode 100644 index 0000000000000000000000000000000000000000..ba3f2de9174d7ec639c450f76578f859dfb5c77c GIT binary patch literal 426 zcmV;b0agBqP)SQ=qmuuFN_BB+#xNkSv-tnFb5dZ<$F|7+H%l|Xs~N?B#Y+O< z{+%^P`M*jGevKFgfHZzNEB-&HS(7M@7zPY{W}*ZG07n^kytBa?#{d8T07*qoM6N<$ Ef~hHwV*mgE literal 0 HcmV?d00001 diff --git a/assets/img/flags/SC.png b/assets/img/flags/SC.png new file mode 100644 index 0000000000000000000000000000000000000000..2a49518339129e7df1bb6819edaa9cbb07c172ef GIT binary patch literal 314 zcmV-A0mc4_P)?)CRT}#Lo?VWERej+AY{5&%AqYygqh9GIIxM)U{$hc^u)}?Kfyj9Adb$UJ|h#z6+A##-Kq3cIX<`WZZ3{an^L HB{Ts5HS{3y literal 0 HcmV?d00001 diff --git a/assets/img/flags/SG.png b/assets/img/flags/SG.png new file mode 100644 index 0000000000000000000000000000000000000000..8b1c5f03f1d5513c12e472f5227a1e73da5a70ea GIT binary patch literal 253 zcmV8yzO1m|Fqxv4JIH(; zEndXxy#%gDJm10S4A7J;TbJ^5_yOSk_yb@2g`WZ^c@-1p#*}TS00000NkvXXu0mjf D*?nxR literal 0 HcmV?d00001 diff --git a/assets/img/flags/SH.png b/assets/img/flags/SH.png new file mode 100644 index 0000000000000000000000000000000000000000..4b2961be86350128faffbcac6aa168f10cc80e16 GIT binary patch literal 333 zcmV-T0kZyyP)i3?a$@ z5bN*XzyHAiXaF*RDP}U6@t@sdDv-uz07w%I%gOym=7SVYT-N^o_<7I&J9aw#KY79R z|DOx=`$5T*7Kz(pI(@}hOMEX=sko>_H9V-(v2 z?iD!)mS4y)_;<~mvnR2piLKzb?yvWOTR;DgmYn)Op6m3#|8`|eJ&rjOBs}<*u*oKy zKFF>;{a=3dwI;(3Uwh^)LD&A)=O_NJ?_T!#f7Ej(1GbHhNeY%tYz+m4e}DatspAeb z@Zg!9Cc=F01cPx(dXTBa%A;Gv8Sw4rum7!^7yh3(C-A>R`=bBO9Sib}N&f%ks@wnT*LM6bo*w$2Bd_{DL)Gg4$ZD|~z*w{Czd%9K z|DejO{}%0L{~c-#|9dtB{O2x7|Ig5PfCvNXcKv6oTKC_7+MfSu9TWdoPn-4MuoLL2 p8ldfshX@-$3Z|X`gCaALjR9qH3`16z8y)}v002ovPDHLkV1gRMlXd_A literal 0 HcmV?d00001 diff --git a/assets/img/flags/SN.png b/assets/img/flags/SN.png new file mode 100644 index 0000000000000000000000000000000000000000..a4fc08fde0781776ab15590fa3af7b903f82ac9f GIT binary patch literal 160 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`>7Fi*Ar_~TfBgS%&#XG5QH#xB z%dP+J$LjA}Cm!uzIyLZvfWd=9vhD_Iw;S5-_9ggiVqAUB;@|U^P9J{mR!maa`QU-6 zyM_D_wVT{?f6M$2e_OziEHFETS1tjE3}F`Ymd=!8(FIz`;OXk; Jvd$@?2>_>zJfr{s literal 0 HcmV?d00001 diff --git a/assets/img/flags/SO.png b/assets/img/flags/SO.png new file mode 100644 index 0000000000000000000000000000000000000000..3f0f4163e4d794b034d65c1ea3141b74faee3f9f GIT binary patch literal 192 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`?Vc`OX3@6B3jJr|(*Eq(w!?#ExrvBUG|C{;uACx$vc9LO2yyLd2ulL!L1*Gr( sui*XgV(<6)5{9o-mo%RDnUcV8ZOMak+;{VD0-eO*>FVdQ&MBb@01dWLGynhq literal 0 HcmV?d00001 diff --git a/assets/img/flags/SR.png b/assets/img/flags/SR.png new file mode 100644 index 0000000000000000000000000000000000000000..6a8eea245d459299bbaa2c14103becd23ca2e771 GIT binary patch literal 166 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`xt=bLAr_~TfBgS%&#XG5QH#w$ zB=es-d+_;&|4*1Z7^k!N{7XK(y=TioiRm_u%yCaV8x2pg*?bBA_y4=b<^TVmOC0!d zJ~3^Bgs0w)?A46s3`f*%a-YAP`ftC^B$i|W>Gg~v#}6&=@L--ZQ{y0mjpAhAXB?3} QK#LhXUHx3vIVCg!0EF&7NdN!< literal 0 HcmV?d00001 diff --git a/assets/img/flags/SS.png b/assets/img/flags/SS.png new file mode 100644 index 0000000000000000000000000000000000000000..c71cafaa177eff14c34e394c8779b0179685a01a GIT binary patch literal 289 zcmV++0p9+JP)3p2{r(Rc`SSW zGxI5vXMkbsbTBRKwc&sH^h^K0eEa>M06;Zh`Er;4?USSb$5vE;`D%{y{?AtO_-|0Bz)!F*N6`Tu7r zdlGMecI0d@E#wIagU_Uf0SNPkO#9DS6%JU%{Y?KS00000NkvXXu0mjf`!9zJ literal 0 HcmV?d00001 diff --git a/assets/img/flags/ST.png b/assets/img/flags/ST.png new file mode 100644 index 0000000000000000000000000000000000000000..480886cac5a213891dd0c1fa37d8c4c12e975a1d GIT binary patch literal 243 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`yF6VSLo7}w|M>sko>_H9qZXUW zvlsuxqgMMGwMiE^o_Mm~yIb!kTa1vw&-$24KlZ;Xk^RKW@KE~JuK)jK(*6MnW|?}s zLk)-iR|<9Z=P_{HFAn+t|2>a#Okk7zD#l((79NI&%Q=hw|DTiiW4`b?h7iVu_4AT` z#EURLyuMVh=%8i?x0tE%pZNhy3myOH3QPVwzcTs9{&F=w<^zcZ6IhR)tT3$7dEviQ rLF~>fO%L{yH$|2mX*};UC4u1ydkx>oC7VA1eZk=A>gTe~DWM4f*coJg literal 0 HcmV?d00001 diff --git a/assets/img/flags/SV.png b/assets/img/flags/SV.png new file mode 100644 index 0000000000000000000000000000000000000000..b5f69fae826585990d347ca13eaa23a99be600bf GIT binary patch literal 209 zcmV;?051QDP)a z1)`rGk@B=b+=BQ{{QLStM;BF3I;%D zT;DTm-NLo&7O%asea6dIcdL8%%j?wxo%HU(rl*HHo*wSpzjeA#`eLw?+JTPz|NqaA zAJ3L=-lyR;jX?wC$T#o5RZlo1VAj7-Ma}>eKqWzb!3-iQDk>uNj~+aD@Th(w5d8Z$ z0gNY3ti6Bt?%n&fLW&9sib9MxXWj+siScxC43Us*J$O*G$w9#7;J-`SlMg?0V0tpY zL2*X2x{bw|74 qA1vX%CcH0W(PWc`a0lTR*ZA$q*+iH=E&mI&hr!d;&t;ucLK6TdirOat literal 0 HcmV?d00001 diff --git a/assets/img/flags/SY.png b/assets/img/flags/SY.png new file mode 100644 index 0000000000000000000000000000000000000000..dd5927a6659e0587b21124e207a57a688128a0f6 GIT binary patch literal 161 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`8J;eVAr_~TfBgS%_>1+Y> zInGso)!F%#nEy@Y-ypr=(Y=%Zk8~uOCVbjz`(N^z>Yw=qrQ$I;7w0ZcFck2;&>$^8 z?|=OF2Rt*{8r$RV{ki}9jnoXaQ`{=;i~@ha#%LU5&Szjaq_KO-Lzmh=Ksy;cUHx3v IIVCg!0GFISXaE2J literal 0 HcmV?d00001 diff --git a/assets/img/flags/SZ.png b/assets/img/flags/SZ.png new file mode 100644 index 0000000000000000000000000000000000000000..b0615c36332d1dd98b1084bde212699e5483d0c5 GIT binary patch literal 366 zcmV-!0g?WRP)um9HY;f=Yly5STk2kEaf_fKXq2caa0wb)a?v5udrj$% z4z(oi4N-%BfcgSYQAt#W07lMc%ISJf>3I4tTKr2IWB?wv^aPbqZ>_kGp9dXHI7)B#9?7tZeM_(0t;x z-KKGIPB|99-A>|$$FPO?7B~UHSz?(&a)P5N4HXV!S{4iQaV*QCP%P5=c&Bla;bv=w z?Akh|kslLO44vcIO|C{GRQ)<;Fvw&y%6KG#Td(udYSGB$sOW6%rE~VcqRE3|4 zG;ox{m6EL2%c|GQrD@Vkrzv?nlnsd^B{kveyg|`7`yxCw-rfgS*%y0RR91 M07*qoM6N<$g3hF)0ssI2 literal 0 HcmV?d00001 diff --git a/assets/img/flags/TC.png b/assets/img/flags/TC.png new file mode 100644 index 0000000000000000000000000000000000000000..b17607b910733cedf84df9ec2bf73cb67142dd99 GIT binary patch literal 312 zcmV-80muG{P)i3?a$@ z5bN*XzyHAiXaF)G&H#`m7?zX!kIV-te6cU||IbzC|G(`K{(rfT@BiJc0jLJR{EOm+ z6)XPt_U{ENe6lOx|LN0_DE9yP5~lw<<~pJpfX|;G>>m>G|I#V0|17gr{+~U=^FJUof;0n|4SN1JOtSy) zyG#0i%_IjfpCkhyhIIY+i!c4p0;DlwffxhuMkB=r41&x=0R{k&?v0>OXq#040000< KMNUMnLSTY%S&rWT literal 0 HcmV?d00001 diff --git a/assets/img/flags/TD.png b/assets/img/flags/TD.png new file mode 100644 index 0000000000000000000000000000000000000000..787eebb6b495642a16ef1a0ee1f2e79e887ca047 GIT binary patch literal 103 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`dY&$hAr_~TfBgS%&#XG5QH$-v zi7$WLkJsyUEqqYilqFGdr6G`+Z6+i0{FxdD8TQ<+5&IC(SO?U};OXk;vd$@?2>|0! BA^iXV literal 0 HcmV?d00001 diff --git a/assets/img/flags/TF.png b/assets/img/flags/TF.png new file mode 100644 index 0000000000000000000000000000000000000000..8292904513b625ce2459aab319c92aa58a4a5aa8 GIT binary patch literal 224 zcmV<603ZK}P)s{SHmUx9@a+Bncb~ld|KQpC|NGC} z1B;R51*fz*|L1Nv^MCs4lVG-hT@&#Jz`UUm*ayZfbM})Kh%g*nvh=@a&H|j#IKY{S a5)1&STz#S9;g&uC0000FVdQ I&MBb@0AAK1Bme*a literal 0 HcmV?d00001 diff --git a/assets/img/flags/TJ.png b/assets/img/flags/TJ.png new file mode 100644 index 0000000000000000000000000000000000000000..b0b546be22ad96260469e6d0477ea9b1acc5c0e3 GIT binary patch literal 203 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`lRRAz$hQXPs@PQbGr7D$dGR7%(e0d~vK(y3uu^m65Yz`ilit z>5CRC@-|g8?Q%HHS~c4__Q|@b2ZUN|8GhYZ@V)fiVZHPFxS7QGCfHQXO`NVd%S%V@ z;JmkC1`dC_o~W&mUR>8%yQ{IMMUThh!~gVGhqli#;{an^LB{Ts5 D^72qh literal 0 HcmV?d00001 diff --git a/assets/img/flags/TK.png b/assets/img/flags/TK.png new file mode 100644 index 0000000000000000000000000000000000000000..b70e8235cc28cabff0c78ef065e1359e88058460 GIT binary patch literal 260 zcmV+f0sH=mP)qbN2Gn2y4?@JjmsXg}}&`f5AuF2Lu^$i%?^-_J7t|9g?)e@U?mX$F**ID<99 z@T^`#{03}4c1p}X%D8T?|pI=+*KeYY;0000< KMNUMnLSTYtjCa%k literal 0 HcmV?d00001 diff --git a/assets/img/flags/TL.png b/assets/img/flags/TL.png new file mode 100644 index 0000000000000000000000000000000000000000..b7e77dce5ffdcf13cb592abf945a298a3072107b GIT binary patch literal 277 zcmV+w0qXvVP)m zeVy|EdwOIT0K#(OEdMVpk^ld9x7zY#19Wz} ziVCI+5|xM#0|{Z4|7YgQ{r|fc7zX*kFwi0)40M%&v2Yj~3kKw*gB~)|!MC{-ro@5I bOq5^%##?GOT}O0D00000NkvXXu0mjfk4=D0 literal 0 HcmV?d00001 diff --git a/assets/img/flags/TM.png b/assets/img/flags/TM.png new file mode 100644 index 0000000000000000000000000000000000000000..e6f69d734a596250dac16bdf290b9236d4c9f12d GIT binary patch literal 392 zcmV;30eAk1P)G z9K{g4ULnB}#h`-fEAx|c_?}13v((xxx@q!_eP;DnRFa$hpc zrg=;Tcq85cKt_*^VwvfJQg7ARlhWvi1fWg`E05$C*T~<1b~4D>5eWpGSQ^I-lA{Nr m5Ok^9-|Ziee}AU23Elu|-h10`h?kuJ0000MX81RgN=l@Gdwg2BY zZ2kZ1#F_s;4;=abZSg9w7)XwQ0fPUZ^GN*vefj$TkJ+XF-`IQp|F&q^|36QjfyJJ4 zOF-1%GT<2l`~MH=#s9x8SpNT|u=4+Z-@gCBJ1^^D> Vm|E^0gx002ovPDHLkV1nLdh&BKK literal 0 HcmV?d00001 diff --git a/assets/img/flags/TO.png b/assets/img/flags/TO.png new file mode 100644 index 0000000000000000000000000000000000000000..f96d99646cfcba13dc5a6d7f9522997808242881 GIT binary patch literal 114 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`mYyz-Ar_~TfBgS%&#XG5F^Wxq zg}u@1qM;x21d$6j8FIQ3*e)J#Xyj+!v{-QZea?rDDF+su@JUH#;C#DjZu>j7IY2`g NJYD@<);T3K0RZT|B$faG literal 0 HcmV?d00001 diff --git a/assets/img/flags/TR.png b/assets/img/flags/TR.png new file mode 100644 index 0000000000000000000000000000000000000000..3af317d9f7c16cf61393e13fff1eb11a871fe335 GIT binary patch literal 311 zcmV-70m%M|P)RHZwEF*dnT7v9&zk@LU0UA%rxL2f8}Qmc^8er0Z$J|N ze_g-%|5N|e|DPsI`~S+(o3H`TR1E(A`}_s0`OD%J{~z;8!4*DJ()<5h-3Ubt!+;M} zjbP1xKYslGL`(_8wpT#ceVMxm>^-aoe4a83;`ED`F*H8|+WuqzA+Y?roDv)^sHlgy z;NAQGPXuHzUEmi14ilU%0EPaa7caqve402NMe`GJ<^NCRweW`l*cKPR|9_r70~_+= zz~TQNYFohgg_#2h=>Qn-Z{yOy8s9`GV+~Ne29T8!2R<`Vf&uv4pE3u^Py7G?002ov JPDHLkV1hgwl6C+9 literal 0 HcmV?d00001 diff --git a/assets/img/flags/TT.png b/assets/img/flags/TT.png new file mode 100644 index 0000000000000000000000000000000000000000..890321abedd9316d582ed3e780d28cf169a619a0 GIT binary patch literal 358 zcmV-s0h#`ZP)l&`>!m=zBMS=DE0#EHDJ%CMq+{33@APS(dPE8;;|ksw%9e zGd%2d^Z<&tv3NVD;4~~w*9buI-7M~b5blM=Jv9Pw^K^ht&=ERAhv*a?(*mgH!xPCu zp68Mc&+{ZJ%TWPlM+=~sMi$e_r!cLopY(3&U-(zR7u^e|hZu+nKL7v#07*qoM6N<$ Eg4w>EG5`Po literal 0 HcmV?d00001 diff --git a/assets/img/flags/TV.png b/assets/img/flags/TV.png new file mode 100644 index 0000000000000000000000000000000000000000..2ec3160533d3d03d529fa5f7a8f5707bfca8624b GIT binary patch literal 398 zcmV;90df9`P)63;8os8E3F!*ptpbdv*6Q55++_&pUb zs<+tC3APV*LUA2)@+$+yfXHvcf-IvYx(u+?y@Bmku|^KD(mz2_c|uva=PGEoFdPe{EK^ix0f11R9@H1tIPxNg7WlL71H~;_u07*qoM6N<$g5%4ouK)l5 literal 0 HcmV?d00001 diff --git a/assets/img/flags/TW.png b/assets/img/flags/TW.png new file mode 100644 index 0000000000000000000000000000000000000000..26425e4bbbcfbed118bb78caab959285985f2707 GIT binary patch literal 205 zcmV;;05boHP)WzV!dhnOFZ$o_y)QoZLdv z3=k5U^MB5qYybcJ`S<_l&%ggCOt=Vk4M{F&Z$Asx{O#NC|JBteNOJ+m&k_>z|F^W9 z`CnIe^1rC)JkrAeggH59q68pO2H;Xa2#*?oWWc~@CQ2{>Iay^LJq85Y00000NkvXX Hu0mjfu2m>s3nEgL{S`Mta zK8qWy5rn;L*$5jDzQ^$YtMiio-yRhOYk+A683ZyAuK_KGH2?nuD!Q;n=)Z~_Ggu?Y z=^!tFG*8)=f!zR&Z5IExo=^lUTHMa}pOc*ttXW^3<^Sz%!v8;?m;E2JWy*hs#dk3c zu-$3;|JGTV|KHAt{?Ci%f;&0JhvWa}Q= z*?;CGw^1~s7ywe#SH=Uj5v1A9gca|oS?o~2jtYDO_&;4>2?7y#rKeP9Jf6aWAK002ov JPDHLkV1izW#_9k7 literal 0 HcmV?d00001 diff --git a/assets/img/flags/UA.png b/assets/img/flags/UA.png new file mode 100644 index 0000000000000000000000000000000000000000..74c2012216fb8298169f83d76118365819405c5f GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`x}GkMAr_~TfBgS%&#XG5QH#yM zV9sBEJ~`8dS!{pyXn&h8W0S=&iII8!OpSvKPaaN@U(2H_2Gq#l>FVdQ&MBb@05`oI A`2YX_ literal 0 HcmV?d00001 diff --git a/assets/img/flags/UG.png b/assets/img/flags/UG.png new file mode 100644 index 0000000000000000000000000000000000000000..c8c244364b0cb7eccd8999da0d8d264d04057cca GIT binary patch literal 188 zcmV;t07L(YP)i`C$YA0000B^b|@u-Oq~aA0Pq5D&wLt0(RH TZr;-Z8pYu0>gTe~DWM4f!MP?3 literal 0 HcmV?d00001 diff --git a/assets/img/flags/UY.png b/assets/img/flags/UY.png new file mode 100644 index 0000000000000000000000000000000000000000..9397cece5fcdc795061f08a59ee858c46189b624 GIT binary patch literal 216 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`^F3W0Lo808oovf>$U(qu`W*GB zYWc6RC@l=rlf^C3>V1P-2L#((11zce#jKo9q#D` zPaoSb%skmSy>?Mz^II#I?6+(le3J@ypR)NNv9;#h%X@xTcIp3Su~=%Y)_8u+OT)Jz z9(|Y2I%u}W@OB)sn(fM;Tx8C?;rM0om?Xyc+bkqSPVASM!uaE_vFJPA35)fXek=Ny Q1#~Kdr>mdKI;Vst01k;%-~a#s literal 0 HcmV?d00001 diff --git a/assets/img/flags/UZ.png b/assets/img/flags/UZ.png new file mode 100644 index 0000000000000000000000000000000000000000..1df6c8822e05802bc9eef10322e6b05e2e7a0a93 GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`S)MMAAr_~TfBgS%&#XG5QH#ys z%$;BIEzj>%{HcH3JpF%SjX<7N_3!#ejok^BDTM;Z3?5wgT&}_8Fe_n}qlqK4!?uGt zO*ead9-BOvv*1>JkwKBdByP5DMv?Z$J7KH;KY8Goa^OI^nwJ5Cob0v2{!L-3KwB9+ MUHx3vIVCg!0F$vlUjP6A literal 0 HcmV?d00001 diff --git a/assets/img/flags/VA.png b/assets/img/flags/VA.png new file mode 100644 index 0000000000000000000000000000000000000000..25a852e90b741c0a9dfa22fd0b2ec53f1b924eda GIT binary patch literal 202 zcmV;*05$)KP)#*f4G|)C~Cb^%W4mCeMJ6Zyx-=b)fqH?E@A6zr49dngM_R{`-IN z(v|;L4$k=h=s@28PZx{-|Nr}oG#7k-ckTc4!~Oqn9@_l>)4M06d*R!Yeg7Y?Y5sp@ z)vEt5pFSqtfPa5}|NngN?*C7B?}FJwG#wB&V4!j%01bXIX>C$NZ~y=R07*qoM6N<$ Ef>>E!CIA2c literal 0 HcmV?d00001 diff --git a/assets/img/flags/VC.png b/assets/img/flags/VC.png new file mode 100644 index 0000000000000000000000000000000000000000..e63a9c1d90c9fac4f784046d840e7e9b20d5db1d GIT binary patch literal 217 zcmV;~04D#5P)Nklc&+vN&o)2q|HIRA{~w=`{m;1|7A!`x0r#K=fY>A%0Q178LpuLg9?%1` zN%8_L;8d1|{g+-G@&ChlDKet*qBz(95Jrwtq749HkQYFhtaLz9N*wsiL)M%-00000NkvXXu0mjflmJ(! literal 0 HcmV?d00001 diff --git a/assets/img/flags/VE.png b/assets/img/flags/VE.png new file mode 100644 index 0000000000000000000000000000000000000000..875f7733f660bc32f490efce29191ef027c42214 GIT binary patch literal 302 zcmV+}0nz@6P)NklfU@-z1#eh@0#Qsl-H~T-O-R%F} zu z2IJNL;n-l+e!~i4KP~upVNHJf0xXI|NEBQ`mgA{ z^}hlTgV-Q3kQ_)2ZUcZeq_v*?pW1TvKZEv){|x#oAy{h#SPUeG>i3?a$@ z5bN*XzyHAiXaF(*DKv{M{BM+F{@*Us_rF!T$$#72od0a*vylt{X@X%nx&O#~kOB57 z3IC0=1pZH5J@0>9neKn5vY7vzR&x*r!2FBig%vCQ_xA4v8(^7}_1`U5@&Bg1oBszF zYyCIRjzV+6^y&Zo3U{CwP~Ej39*FjNdH=0)CI6qhdG&uxo%w%_xRC!$CP-m`FQ7m; zqd4ZjUcTP{X*<^bPwEK%pHLR`pVw?M@diw8@%mrh==a~X(B^+xv-kfQ?cV=Q{X2*^ jfM7HZya9tCGf{v6$>@l%TpGxD00000NkvXXu0mjfF885) literal 0 HcmV?d00001 diff --git a/assets/img/flags/VI.png b/assets/img/flags/VI.png new file mode 100644 index 0000000000000000000000000000000000000000..69d667a5a2973553635ee0613ad93e58f5a6947a GIT binary patch literal 500 zcmV()>mYcD7HsR#2cGHS`7F=x5rEi8i~j|zw|)Yy zQ^T&Oky8FPwXG6Xb?T2T>Dt)!vz|#z};a(fGUtMLEu{4nNxucCmB6pTT>Fu_gs?V}2HE6@v4o2b@cPr6*@{ z`{`ADffH00`pJaOkW?NpbNwM&Pk`d6kI>{TIz~0(W{uu#(F2By1~1Gy+L!n2QMU4I z>NVxWDYAF^C|nDX8&GIuE^;Mha#q#QR~p>S6+ECNB`qnj9h>gq3)~oNN1we+>ZOl& zH;+T=nhC#Y55|@eq0000<=NklJd z_;1F~@qam=JZT01!GC@phX2zl8UDZ8%kV!7XkZ+d;Qu26D#RPml*#Zv(U0N3mJ-AN z?Nb>3UtI$+6eNcW!wrCG2GKCdI3J+YIRY5j1uz3tWEuX00`>MrhX101M7jXP;o)NV z-;xapgQ6IQ|7`02F9i8qEJ0 zf>i%AOm-qlGl~HWdLsWBnyrb}h+)9MXC_K80QrGO6CeuZy#N3J07*qoM6N<$f*`Df AjsO4v literal 0 HcmV?d00001 diff --git a/assets/img/flags/WF.png b/assets/img/flags/WF.png new file mode 100644 index 0000000000000000000000000000000000000000..922b74e26de3a4eac456e0d7b4d617ad62b23d97 GIT binary patch literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`wVp1HAr_~TfBgS%_>1+YB zMDMD9%?3Pk#4oZXr^j&!2J-yqXKxUxv*Wt>CF`3|N20sm zLaB!HGYzKinYH!F|H{C*|JSGKwMqMIVKhi@HffG!C=v8zJ7Cgy_<_bW1BDcZgAo!H f%xreXflZ7qfy*mQeq7QAI)uT~)z4*}Q$iB}ND)G^ literal 0 HcmV?d00001 diff --git a/assets/img/flags/WS.png b/assets/img/flags/WS.png new file mode 100644 index 0000000000000000000000000000000000000000..d1f62df103e1d72aaa885db5fe5ceac77f2fb34f GIT binary patch literal 236 zcmV;E%o0kLZJf4#tI|Mh~V{bx|F`p=+Q_a7v8PEZXiMlpcRpy@vs z5Kmiv`hQmArvHl0eg8#mI*B(xA86~kJ=gy;C{+AsP^$XBaMOkV>fV!xH-Jf};XjLB z<9`Ol%Ky^Nz5j)+JHcKe+5j*HYOsr&_uoEY{(lCwn*T6yk_=e3?b3hm%%%SsRH}(L m0G9$nc+>zS0|q`bQGx;N|6wv`=fMB~0000S-SU literal 0 HcmV?d00001 diff --git a/assets/img/flags/YE.png b/assets/img/flags/YE.png new file mode 100644 index 0000000000000000000000000000000000000000..bad5e1f427e2478ef83ee6a87cf76b815c42d39b GIT binary patch literal 103 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`dY&$hAr`&KKmPx>XI7n|bhd!` z9OtUP>g@dL%>O3yvvo5%e3&m|GlypZ3;!NQ2KluOZ~3i1zXs}L@O1TaS?83{1OPaG B9rgeK literal 0 HcmV?d00001 diff --git a/assets/img/flags/YT.png b/assets/img/flags/YT.png new file mode 100644 index 0000000000000000000000000000000000000000..676e06ca7972846cf433a1d18fa87e645c848a65 GIT binary patch literal 482 zcmV<80UiE{P)|6otRPGn&!Ssn&KV3W`V(gcc+qUD|=V=)R8<@(A4*`U>Jhh~TcxxTqwHiW*HS z0Rw>$Du$*@!>#zS1?lVvT+vNq@1Ay_O z7Im;#Ea-N-gkea%zR$tIM+_r{W!YR`FF8DX&ulhD2*F!3Lq4B>7-GBKCKih^7z|jg zZa6-Ej8b_#&%kwcve_)d;gEPdPN&m}wxDSmsZ@$ut%hl)c$NObv(1p)<|}pUGlB0@ zsZ>a((*!{fX#v}|ab1^UvB>G^7$zV%P>an>WW6A!Duw1*=$Bz==b}SN+ss=IZ{fJ#vZ=6K>-Of$!hIk zSr&ytfyrd@8zLUm*z5K1JP#=)3Oemj7=)zW@JzVjFP=2=Oxh-_gSVfA=!A|Duh- zU_+$aQ~#fNdhb6001X2m#mvO;zbTIE|GjlG|4lo*;9kOqQ4E0Lnn9;O*J<-5 z-GHiauK$nLN&UC(^d!v%Je-XG*Vpm=Ke$5WzXUW4B-&E`AA59*xG=aeTkwD19Haki zwc%jRcC#D*Kl$+Pe_i{b|2#51I5FIS$O(>cXBBN<{Qvd4umAlbSL4-;VgN`ZM?>8I u6_<|vKX>WHe@*jQ1T~`=Fz}g)5)1$+q>DKWh4NDX0000JNL{{)hD* zrkrdZOu1iHr2nvA-|_JOAD*`Vai%=~ms)umGIUKnE%8q~O!);1`QLC72T2&&1^;Fl4!TgQZ|36#D z_5U%@6+fO${D1x8)&I-1m56u2?^jR%|NC(9|F8F#|8H+C|9^tiFaUuMpZ@&czs`fa ySU7R+%zyt@GSflN@`nG~Oi-8D=l>t<-TuFoQT_i`Odh`h?^JdFzc&JEa`6T$ z{<&rQ{|{z%|36sR0}XQd|5jM$KMnvf;EuG^{|^rC|KBNT|9_{d2bTFhcj5oH5=sz5 zMP%{20OUuIA~2Rv{QqOw%KsljqW-_(lK|Qd6cbm#Z@_=@0mJ}orW1pyXTb3A!T@I` aN-zK@N0K5gM7~V`0000sko>_H9qZXTi zPwBt>sMN3jckH|L-&*Ta0T1(uBn4B2tqQgU%)j=Q{www?_@jR>^MC!+xmJG64n;6I zhe^@8A^)|v-1?h7^Uat2iV^&W8{8I1yjD@)AT=Rsh0P%b0lVdEf81YrgXztl13AV! z7!}MscvRSfnx8m@FgyG(6A*Wk{+B-M5sx{;k#qfqETak_5Y>K z8UL5`r~F^AF!TScIkWyBy|f-P3~+**Y5#wpG5de0Jpcc*295v6FW&j@R#8TR0q3p$ z|GpUW|MKdk|6eW$_BJlG`*zv?9b0_=Ke>DA|F@H?{vUev`hVBr#l#zM z;?TtZ+c)a}-`4N^|M045{~zw1@qhoDha`9bKcH4#7)S#E^fONxpV$0Z00000NkvXX Hu0mjf7j?a? literal 0 HcmV?d00001 diff --git a/assets/img/flags/_commonwealth.png b/assets/img/flags/_commonwealth.png new file mode 100644 index 0000000000000000000000000000000000000000..8f08c8a01414f54a1e14bc435c33877876e8b1ba GIT binary patch literal 443 zcmV;s0Yv_ZP)Q^t`#A*?Dw5M zN0KC!z(tG0FwDHqd%iPgC;(I^>S(wQpp>_0kUR!T7DE@!L8yv=Qbfu99l18X*Bn3w z#p6L3+)kVX=fGZ1!+Bsu-@;K1T!pm+kl}6(i__37K3;|p=ClZLI+XKrWKt^7*4-LF z(E=bCxq-`PLFHKryLTBzj|0X{2Q1rm#N%ot5~F0nCBLXCKr+c9@i+$mg#l;8MKP;H zA)`bp$&e$tz?i)dskr(vegOvNk5S4iv2kcZE=}XDIE>eV44;)jXlx!(y_xnINVF`H z#HR1ufB7%8mI0A@%Z~d8H4kr_LG?io z$srJ)!8-{u7}4=6=M>03(O{N#+JA-b5{V~*SX^J_gS?7N!9`s4>;KY&pG_nZki>;d lq_&Z7VzhY^|Nm#|L|@lpUw;-Uz$O3y002ovPDHLkV1iHCy=DLa literal 0 HcmV?d00001 diff --git a/assets/img/flags/_england.png b/assets/img/flags/_england.png new file mode 100644 index 0000000000000000000000000000000000000000..7acb112f0a8d30a47164ce7cf83998fc8e9473e7 GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`x}GkMAr_~TfBgS%&#XG5F^VmL zT_Kb0&ZWbr7-l#H8GhxF=w)3cVenyQXATd8oS)T)=neW#K#dHZu6{1-oD!M>wn?;TmK^#ULno^kh5Xzi3cBpU6Zu@`v1s9 zS4ek3_L`gj9j2W7pSk+R{|9fslNJU+^Dcokmu$TKf5y=#|I0SrAw3qJef;_VDbPzW pOuuwMjsfK7h5!HmQ-mot006+Adp@~ff^7f*002ovPDHLkV1o3ybPxal literal 0 HcmV?d00001 diff --git a/assets/img/flags/_kosovo.png b/assets/img/flags/_kosovo.png new file mode 100644 index 0000000000000000000000000000000000000000..dfbb5f01f64320db18e1b3b226ce75b5b7143d9b GIT binary patch literal 434 zcmV;j0ZsmiP))td5_u19Y$8J z!OdNwW9$r!?*jAD5}eCd2$4K(X0HH9f!02OcGoglD9r*at^_vM*U)TFquwm#c^KUV z`77wiomI9R8mxK5!;5$?FyIqjv^X*}CV5g7Kn-LCO!#Bu&@>)}0jd|V7pYhPU8DPC zQeGaQvgP3>fVWc?Pq}_P@Pky+J{eh{d0BwrYl`UBD((I(I`DeJp_F#QFAxU>y+d=HI`ApZ`^r6xvt13Ty(K0sZMNT;f-kp)Q7lS%Ytrd0*} c|7WU2A3=wOIzKC?Bme*a07*qoM6N<$f@+7pivR!s literal 0 HcmV?d00001 diff --git a/assets/img/flags/_mars.png b/assets/img/flags/_mars.png new file mode 100644 index 0000000000000000000000000000000000000000..4f5980b7f498758e6b1645e544ea7e4eb57bd60d GIT binary patch literal 103 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`dY&$hAr_~TfBgS%&#XG5QHyPk zVBp{WzMW#?ixW_n( BAiMwo literal 0 HcmV?d00001 diff --git a/assets/img/flags/_nagorno-karabakh.png b/assets/img/flags/_nagorno-karabakh.png new file mode 100644 index 0000000000000000000000000000000000000000..f5a8d2718e2a4e302ec36fba5240f4ff6ef207b7 GIT binary patch literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`A)YRdAr_~TfBgS%&#XG5(Tc4> zBgs%}!E(kIw-l0Gq#AsUH!Fi90&koC8#jvB_3#87?KEMS ouBal}a8}`!q``-oolHCop8adD)|!cK2Aa>{>FVdQ&MBb@0Bs&F2mk;8 literal 0 HcmV?d00001 diff --git a/assets/img/flags/_nato.png b/assets/img/flags/_nato.png new file mode 100644 index 0000000000000000000000000000000000000000..fdb05410e34f14af3d7e8e5c9a1b4c0eade57f63 GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`VV*9IAr_~TfBgS%&#XG5QH#xB z#g%{R?7}7sw$w>?p-+(buEyDM|t{v7#uXgMkm3wY9XZyUsp*%Q;A} zbnLRmYeRUH1 zWfQG^$V4p4=}ertM^z%-!eo37+0dy>NQ-$;4Bg6X;H<$ zDF1$luJ7W%Z4h`SfoNmtduWXut&Wc6+H5tB`rG*P76SWDA`<$pAIz#mdg;}p`wss9 bGYyFf!% z3EUYu2P7AKkz{LQG+gTe~ HDWM4f9YP?4 literal 0 HcmV?d00001 diff --git a/assets/img/flags/_scotland.png b/assets/img/flags/_scotland.png new file mode 100644 index 0000000000000000000000000000000000000000..db580403dc74a5ded3d7f3fe35388fc1324745ce GIT binary patch literal 351 zcmV-l0igbgP)!JYy-u_v;$92DmaZ`O$RXE x!?)l6o3)+BnGy|}PX5358hc6{_{>B$1^^GQ95S_n#OeS5002ovPDHLkV1k|gr1$^; literal 0 HcmV?d00001 diff --git a/assets/img/flags/_somaliland.png b/assets/img/flags/_somaliland.png new file mode 100644 index 0000000000000000000000000000000000000000..a903a3b7335325525071dcffa8297e9eaa37239f GIT binary patch literal 315 zcmV-B0mS}^P)Wc-yYC(EX3=o>d@ZWWV(f@-_cm1Dqq3wUo@x1@jFZcXkczyc+$rn5Rx128j zzvcew|Gt~8z-loJ5S`8N-(Z>4|M}OZ{9kfo*8i4M<^R{;T@E&&?R4e;rcJ%b z0sqMd_zjppfByeDbLNn5z<~n?z?f_Ui1xxcLAC$n;6Y@-z-J~(FaVu%;1ARDlcN9t N002ovPDHLkV1j;+rD^~G literal 0 HcmV?d00001 diff --git a/assets/img/flags/_south-ossetia.png b/assets/img/flags/_south-ossetia.png new file mode 100644 index 0000000000000000000000000000000000000000..d616841b68587ce5643440e9fc51899759d0acee GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`+MX_sAr_~TfBgS%&#XG5F^X*h ycUv#x{uwj>Pi|(P$oNFP`C-Zdg%dt0$qX#hxxN0En=b}xWAJqKb6Mw<&;$TCF&`8F literal 0 HcmV?d00001 diff --git a/assets/img/flags/_united-nations.png b/assets/img/flags/_united-nations.png new file mode 100644 index 0000000000000000000000000000000000000000..8e45e999869e54dd784a136f3fad1c5600da818d GIT binary patch literal 366 zcmV-!0g?WRP)DTyhROm~ob$Az&>Oi&>iPvMGzEjqA5^iHb&$@#qK|SdDR`+v zf3!e5urWzXo1oDHw2y&ULFHWC0e9&N2=#!4ZQbo1v~`L9|Cu_GFG7Rq?XI7o@@X^+2 zkwl5d_A_U+v8}nk>ver%sjZ9!>qvFJsMXmU8jb*CWye z&mM*D=+(ZK&nnGOBzQ8B;WOI_flW+>*Bck_nB&(T!H^iB(6q;(Ku_T)!>7+oM|VyP aVrDSUR%x|P6txCAfx*+&&t;ucLK6T}twc`% literal 0 HcmV?d00001 diff --git a/assets/img/flags/_wales.png b/assets/img/flags/_wales.png new file mode 100644 index 0000000000000000000000000000000000000000..51f13c2e9570d380fe129aca6b3bf7a8573915b0 GIT binary patch literal 527 zcmV+q0`UEbP)%gb8;+8SzoD|&jZ zaaSY0f_;SCUV?{e30g(QizEUUnwT645IR%O7iGx!249yJc-+^8a^1_zk}CW~A|vL5 zj9EndJJZd(9K5mEQOzRC-43*bZUhKVYRIVeY6YEQy1{ao_Hv19$8CfQ?VMpG;w|leQ`ecby$`f6hDBU}s~X0bsc=<2o-Q zd(r`=8tXkLa+5@b$3~v7n&LZ^oVqA+;D+Nj8)P)_Q~iT}1^lJ9^)LKCz&bv%xuciF R1tS0e002ovPDHLkV1g|__Nf2> literal 0 HcmV?d00001 diff --git a/assets/img/linux.svg b/assets/img/linux.svg new file mode 100755 index 00000000..deed3874 --- /dev/null +++ b/assets/img/linux.svg @@ -0,0 +1 @@ + diff --git a/assets/img/mac.svg b/assets/img/mac.svg new file mode 100755 index 00000000..6ea43ad8 --- /dev/null +++ b/assets/img/mac.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/plants1-br.png b/assets/img/plants1-br.png new file mode 100644 index 0000000000000000000000000000000000000000..7027c2fd37fde1c11e7cae55a4432f5b2e0bc818 GIT binary patch literal 25340 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 literal 0 HcmV?d00001 diff --git a/assets/img/plants1.png b/assets/img/plants1.png new file mode 100644 index 0000000000000000000000000000000000000000..8e6af5ac7bd85b4bcd1ffb4670229e4ac6413897 GIT binary patch literal 36805 zcmce-1yq!6*FQ=Lh?I(eAW}*X48t%%H%NCVHB11K(jcjXf|QiBbcdiIjUpu_AR^LI z(j_Tz?s@8czw^fTtp7Rd&sxaLeP6NnzIOcf-j`3cHI=C-m?#Jc2&nF;DCh!zrwIrM zq2#2%r&6lLG2n}dt-QRpf~~8wJ<6L+TZDjsCpJAwQbkUOQD$I7?o%*_olX5G8Y=oz z${J{{EZqezs{6ycM{$-O-cp76t%)6MOpo%J?%KtZGBa^7h4V0F+c>B{dHEe?{6qhf zNpsum<4t1aq%8e%LgU_XLktH`&a1a>lC$qal00%z^q-9p5>#6vtF&7_5d?2HMB+cD zd)hNJjHXyCdE9j>&lnFsi4srdd(lc314+O>Rv}97si~5+Cy?T2ig^6>f^Tv$D^at; zh!$a{D8t2|x5T-1mbYXZ3C|SF20kAhm=O$k)x31+D5j)2-(Hul)Z>TA*xV_*JJ$OM z)EWOVZyqzXcF>a*^o~FQ{lH|hyqAMu&|k-HH5z>P%U#ydZ~E7xh-L&w?rxI5_L?cp zrR*VSy@(jH^+FOjQQj4LCuyNVLqo`x^61myOZPl7(|cQWMSH(8zGVKIX$*@yVPGJ_ zZmu=x76P)q=&WMmNvDwhV!Dfq*!yzrj zCgCLpEZ~53L$P@|*gLw4c}a5owyqfP8UI*_gYCB=Zg!F!viJ?zj5V~`Cc&r--1Fi?xt25{VQ7Lxdm@5HJGd>h0);@&Y-!a{jr30@fAnV(aW? z>*UCWUlC>HFCX9|JF*kw0c(6;N(i zNe=u7kgzaFSX5saA|{FugCh9B!eU_X??W{J{luW$P=5}FfI(mwNEoayEGh;O7K1|h z!H|CpcEZ@=y#Lpse;vA*ypz3?i-r>hphJX%?H|wY0M5_F31@2$y!eL`~#c)n8 z4k+LVZ5>e7SRrRe>)+_q&=9-h=<0@YL}TwLNOAy73fkIY#4uPi91cd}3C4L_`#YMhHV-f6iBMLc8PD3qSvN^Tarzfi?a?6&4AFVsJ1B2#bRQ0)s+D zKqw?!1OyRAS|K5592f;d|GAo$i!Ia-1PwvKK?oEE3xdL|P#`1% zutXFV1wq2GFfcHHgAK0~V!$#0NhrJ-N^%JQ@z5Broc8X&KZHZTz>knH{@*{|oBS{D z|MA+u(H0OTgo6#QR==Ny9^l~ssl!A?MMVF2u5IguwKrC<1*GHp8zF%3{`r;3KfMwG zBk(TdA42<60J6Z*xB{x``iF~>Ls|dv$=+59I3fjkFbpCGRe(caqDVP8Q3OI%R8AJ5 zBnN>h2}=m!eGoA4clQQ-5QQp;h{`DMuP zg2SwEA{bGqC>Hl`cK;7+A;BmN9037|0PKQ9aZoG>1;q)2kRn(tN(7Bx3iG$=!ir#E zC?paDLxJHS90qCyECa)WL{U%-1dPOB&}ih}UkiiAVX-)XE)gtX@PMWxaVUUgFbo04 zSOGSLw)(qkK~ZQc7y>Q|0;2#M#GnCrA+2yI5JDJ*gQFmD90rW~yK5nFU$xuoy7x z-{{@HKSwOe3XH`9vd3Xy09`MTO;sA}`?o9*&hlrxF_*1dM z;SULLlU6tg2#^67j>TEQk&u7Gz@M`bNHiP}0TLt%g8%}=!r>qk27!0CFf0@WXf&XM z|4QtbjsPRU|7Era5)2U$wgSOXz}`?%K-efK1W+xks4&I~493Et|7G^y z2@3ytZ@j`HZ~)(cfWaU%o^m7{h$~hi`1k-pAh6(nc1pjw)89R?(0_N;fM9{xs$z?G z-QIt=Up?%DzZ@~!Z-`0^h5kJnq49VGi{bd=vF*Q$$$u;Q{l2WH4Hk%d{{^D_KFrk# z=jMrW!OB_#!Se6nAff-nfh)@6ztayv!bAZ@1V9rMi~>Q0F+d!LiGl$Dg~i|yR#+$+ z3Ix>uLO)VW81g5?`ah=sj}6c^C`W57kbnqr{GW3KL@$ID3XTK}4RD80vkHGtJZ zfVhUTLSSJK(SPCSKjOgu8yumX96hiu|DIovNnu@FB{^^|P7Z7+XJ>m`^l!LH z$ioryM}qQ~sk0{r|Y%|Eha`56}Kj-TS|FQos3> z-(7>y|J!T){$1gBaVp^t=(kKt?BDr^zpITvY9$6B^uI9spCRDyI8wZQ;^#a4e?{qd z)%lI_f6ABtd;}!n_>X^O|G<~O>IYayz-C>5>VR6)VKM=MU-}&dS$(gx^(N0WjFI=! zsU%!KD!st<;3r8R4?FuaVJ`1??=Kr)N7+~2Ft;D$@?x1kx-zEJ#d^H*n4aZ!2s#E4{0^0B|v>2i5-YW**%gS6e? zX-m{@MKzlC`;MF>h#M3h4PV!n?OU~oqzNZT4g(_++mqT*b6SDJ2_C*}b`uSf zo{I63;x4JM1{s}C5;ynEJQ@zUVhEnM-kITS-J`m(;xlt`#wl==Fg=j^hGs#T1yKgE z|C@wSb0LR0JJ;KgH4pyiV!i;vMj~edj~TI<$=2%DQyI_5xkidZ`r94%O=47D+`7UQ zNa{=OrN1=+qauG4$l?)CfmX zhZe1``5IIkBuG(pE{*4Rmc0g{LB$mM*XS~Gl`$*mUZU`=L335F`J-?Xnl$C96x5xrcxw=$s zQs)<rxcy4icqCO}KCMR?vXC{fUjIpu{!4#ApXMpw@bcaDGEU#BH66rv&d+(JR} zc{nJLiJIhdyNt{!LyN_!twDQ@^616aEP@XEK~;6>&^5QBrEG18xuJQJqM$Wfm}9P;^U^*3X5A8bSp^;Vpf|aDk#G! zS&$b_jre{vW2)(28Mk|7RE&&X!kg5>?qq8wkrpv~>qox;>cd;Qa^IC+dYK()51u*B zV` zObu7mk{AkZkj>o56VNTnzhdUk!0RyRf>)g?w8@b3knJUQuZZ%9&0+VT{A+X{?^`h% z+VZ^!G$Y$#m7!SKxQJ(75oK~BH$^M!BP<&E$o?fQkz#k}g}gy5B&o)UojcHzgGJ(@ z-1s42RgH~@A*lHf*VO7Draiv}aM1Zh-#{_Dvy?pGfXbma%EV%BQ;bmUG0pJKFa(C( zbEg`yc?g_?t;DzY=)4I3a#(mDaSP={uhjM5ZLRa%VfGcph6ZZ=?I+}{ligLbS zO!+s9n-fu4KA42`^y|JfnW=6SV!zE}9$D%!ZIsh9jMs0D7Qz!o=jL~(u-MrkR>F?p z_G!8R9s#LmYi$SCD)Nn`c)c#y*JM*J+9dX;<>f>~32eV2W^RQz(|b`MLr%D2Kw=%2 zZg)xus;1>v1guZJ@hewsc;3{PQEid1mPdXGuImMtZhQFo%DDXXJRXX-lN7~wkG_A* zYZuiUnac2}sk{G*MA)+aM_DniT?*Hh&q#4-l9S|@-%R)ZlKS8PAu6F_$rV9zGg?<9 z9TaObss0KtDGx3+b5=or_TV&}*o`+D?CCY&XMj7h)f2K-HD{o>Z(yJ);@-6XeYlEe z#A8epe@J6p1^(*`4C2bo@@%7?H=`f9bQ38BonMe3{t#pKEQ9b24o*{fZ4PN>xo_jz zKBy{GSm~i6^DMBGwYO|jf2M^U9g)XT2PcB#s=qW0+pa!Vk+(OQZpAyn@6HX^)b$LZ z^DUcJpObmLB`IHai?;NhZ^qVfn<*~LI2fyA|6 zdP>)7pJH`1I;*kQXn(U>>fu}jiFVYcESLE~LgRG8uln{`+LjmH#wd(yue$%^MsCs6 zHyF_(E zO(t$D9Qj{b;92gjC2or;9&v|6KHGjH&{?|fY&+~i_a@o8UJ({oTCaG^s}a%lI1ql&HVrHKZC&9+X}9qJvr}K$vf971nS%N*bn=L`JGS6|c_K@1XepMcw7fVCmcs zhbi^Y2$0%KwsaouSfx`ermfE}r)kly+M4Gh^&D?UsN48?MSp)r|7`3wI@bBeRkes+ z0&L`d#xJcCk+RDp*JNmmv`;;9bv8?8WgpJlRfq8uhjvl$@yws{I*0W+F3FtJ%8}DE zEIMj@K#vj++%xn}T9iD3mdD1ZM@41^HO4zI2hsD3^g z2P}8_!p4B#$h_a?+ng~A)PPO2ufslFR$)8oJgJnTpk(jTB`_4CdFu*p<$c`=?1F>I@mTn;~mu*XY( zWRA-&q@;{`xeCh{)Vvby#2il~1FuxIi>yFQgEA^se+|i(vCuLvk9`9C`9lq|t!OzN zheY1`FY|C%dhWNsye* zV5#eS)6X9(SAB(@HvIj}Cvxhri1v{1oSQ1k9)X5TnYBNbyxvfS#29SBQlTGw?^UYf zjJ`LCYM7!3Hs@s`QUl)(>-gud1rLz8Jp!CfujTetql3Vp!mtwF=vnQWFFUNA*9BP8 z77VwatWnPPX-js-=?zSD9W-nhP5eyE|rK(BkU59c>?x}ZNZ zN2d|tDgWC#XYtv&KderI*jQN1Ze4i*gMC?%oFbn``_a!LI4^V2b^g@CT>)7lFft2rSoP7m)r zh{2mJ711G&E=`_*nc1Y9n_E?V)%$NNs+=80j7Ki3&50CWHl7a|4r>eo7T7SG6bRQp z{xs`6G&mN5O(`ue>(Ym?$0e`6q~-37^tJf5H?l}Q`UpJ%`~sdsWB5jvoH4r=8|}-A zO#)`-RRIwFMX6Ev@wMxj7a`?FwYB*a3AbOZ&fS?ge?21x#D#ob=nPu_!LQYNO=V^7 z@a)Q(8dWaWBL6r2u|hIg1Yf=b-no~ZMf~Z<27T@VV?)pTCf9G0n75?b@#F*MkTQGP zwe~#RtI?d4H0+7O``&>8$6uv`?iNy?{0C0C&sca<8d61#%w#*R^b<)x?VTt}X4l5& z%)>lz!WJ_JVnva z%paey+?3h3LPYfXyo%{SKJB$SlEk=o=wO_i@)l~=5f-URp8{Lh8Q>+s!^?o}1S@d7 zTkF;QRp|QdA|%gFS!W#x7AnL6hf|(Y+sZ0t{iE!9<53(1CH}eWiZLdrVOBl8C8q zo+gK?uM5&0Ru*`+rtdrCE{n;xU#&`G#w2hc@1axZgU^0#&wQ~{5JZW7@@t{L41nW z9AJm1?RUSOzs%`AbrkYGopBV(9l_fI(otS@cO+zBsdMQdPURMP7<_fSsNdV#XDCP;r1w>W2)n#6q!-AHBSh9?= z%2m%-ejiSb!=yc|YV#EOW-d$-pD@vC8zkg(I%3>@ekw~%T@9E3OnsbI?nGj1$fwp5 zkw!u+t3C|)pBz=MEMXrX8t1k?p>|YllX%=Kpa67(no!r;mFTdJrg5o1nxJJHA7EZj zPu){956HUBU?6>SX0pqzQIi1hy0JNMu$YuAB-YZ=W98cDvu)Awus+;XM7pZko$JZZ z6vdiq`$YGjvmU-yh>h&d26E zc`DD9rA}U5y|e~`xN>Y?x;cf(#9ln8i`a z1P?27B9bO{T!5go#I5DJb5x*0fBiL97JBy!LBKv72An?-Cz}AFFG@6lg`RD$E z={4fUgW7=EvPpY*v%pq#Lw)7P32KQ5eJAE-H@}^FqIa5(Z?_Gpf6~0Fsqd}UlI+0Q zq~ex~YPJ`g>*yLL1*bv;s!WyWt)woT2)U%|GRqJ+$qD{U3cBU2OMdogSwFO7-3wVc zX75B~&Ry6t(dp>3f9E3U}2Nt;`+Hte3oF$F*@E?;P?S1_wT_1+*`GqeoHLX z_}G?ZctX@L&j5;$H$LLkb6C}*3$d&uW+z!?woAAxKKjAYbL;s%3r$Zg))Ia-iLBx?Jw>-DVGf+{k?m* zS=+m_{!B`mY5}i%txr`@qYj}-eRmadaLZzfd7$Vri@yB<3%oi=aA6b<^yO5ur2h^tpa960Ab6rt4)o8VD^bfO6dD;I{Cpzf1h7 zTBg{+SGT)3@J5z8$K}T=AJ}ff?-U5RXT1`*(JGz7C9V59mFke2Lf11!)eN%u{LP&V zqqV`!Ve%F#H)Czatzhl@^bI~uQWK6e_hOyjG_PD6bdN-jPJj5c#h98!mEm)8@79%t z@my;@b@7te)oyS~xADg>i6)2Aiw#}$r$W1$qoc$$ryC8MKbNS5L+zFK6)~c|24Z4j zNT10E0FHi9kLd1Z$Y3`=-Q2m+_+;e>D59Hjtr|9G5+z81HB<_)GmRw7Tf8$=L4n8a z!}A9k!#;H=)0HtZvZP;5(nKc^g2da(l0A=1OG;I%s8aGDybK6*6YD#9Z#v@8lU2+| z#vKKQ`9D#))jO;usd`JdZm847S(U#uol0ltypt*C*ge$Np9FEymtZWphHG+C@`jji zd{mSs4wsMcTN%3>N;ahCGv*Y?AJkbS&yht5o6$>6?HFJie!`TeYyRDTTh=8-AQ5|q zlc(i>BF4x7XZ;hB(iv(MY3RgeIFUSMObJ0Khuu%_uE#05FM_F`Co4< zNlMtGweX&Q8t%~Vk)}qD7mLeJM*o1o<>d(V3`w)-Swaco^FM_7$yVv%3Hh~5 zj|WqeE}UEY-{H9e7E>{N)9;!j{m9vkO-vk(Kik+izmN}$ey)1kdt(L;QGY+wk7txgSW(m4ODpCciX#aZVkOL!*QyxuUj9zS$BA%;Ld z3EaQ>tF%fyXBu>#{`|a&0~4>M>-`Z41_#@w2!I4*`o6T>TwND$b?=Nqr*Ls=Npx0xSnRyd#V?rxC_$P=;1%{wt7k^5cTV=`x) z(#dkDZ5Jp4y*Dy z1>2I;Y;$rrS(WtFOP?rkXidB()P_s32~xm>`oQr&Em8OoDVQZF_AWVcz)M zx>9b}wB*uIcNUtuliXSp7>4PZo&@m~$ zDLVR=Ao~|PUfVTUUGy}*+jbKB6Gi#)6~yn@l%sFvRF|Z-w_oT_m#pS&wjHrhI3ekZ zt!P^0*CMqI&XmC^Hd2hS0r~cGcUH(jb>c&DsuN+C;<~yd22-+%966Od!_t0S^&pVw z(Hxyn@>3qazb8?tv?j8*3FN))&qxz?}Ec{0?&^kKHlP ze_E?b)6y5juE(mG5S-B;+U?^g#wl3~_fcnyyhZ~{yJTBFjlppEQ%3`*wc=GVGHz-S zD73euL(Ez;b0W5B2s0f*ghzdZ7#6eHognG%mcy?tz99GUp5|X#!$qGSrN#lJJCj$5GC7?};iy;uP{=-9(d-pDDX8L6L?tc`Fq&Y;^L_f(q2LOfo zq6LY#n0fi?F|k_2rM6@-7x|vqZ7+n|4<0{1GGjBd*|o9ZYg9Bn#mQ+gbK-0lLbF(w z#z8LI_Sc%5Nqv*~d$wM76dHk&hL@dZcFDSM)e6O3L==iBOUsz2Pup%?x!9j3)!otI zGv?qavV%|MY*e~KtXJid#GtP8BEl6~gchb9d5~uK}o_0uhi(so8LSb6= z{aUMCi33}(3|pnTYZ&VAR2B#_2s zAPl=DKtU$wX7o~LqdDsmk!6TNAg_ETNVvni2i+qaLnr*Ck-mX0^WjK=qpN&Sw8C3!N#w@W8% z&MnyO`HBhm&h7k)Y_S%TQA4>Dg$G;rhM~Ux(kc0`fJ`or*OZNRzH2rIu|_Ru`T~RV zDc5KP z9V>p;klbrM${wDu!GYcsvKVLoL%iDD*EtiNBTu#dqK;|mOfuP zuZkifT`-wEMqU58BTocS`K5_=asCc2!aVPGwKg#^FJ6^JjlfrAHOr+o?St%W=Iumb zsW)W1uPM<#pgUB&?{?VDYiUf;^_2YOxEb9q5bO#p{Pd6FQ*PlUHvRYK;q_$f?stFFgf?Q!Rk)=YrpG)}36 zVC72ge<5lefK@%tE;ddy083RUt11FfY(s^6*T-)H$ov^Y-yL;Vm%`Xyj29kUo&nkl zbL3k!$?e$Yt*zQSd7ey(J?kB; zmYIE_)aBOF7BP8EWWxHQN8dBw_IY;`eS^1a9@|>=f+rfm%u$%?FWjJaSKnJ@n6B~O zgbQl~_2s|h(Urah)I{=%nOM5wU$@vV9`MS_*5nm4v*+%TuqCK|cSbC)i_1vdL_3^5 z*ALF-xzV(NcxOpg!rzl7Ic}WoLY#cAILZ~ToZ&s&$?CVS-|paMR#07im7OFjaxaBF z6i7j4IhD7){Y()7QANugR5VPUHFOmRGfbk*5{OQLTQp{-n5*RzvT;5>f5l zM-HA!z+D=3Sp2*dfm6fM+0{t3p!NAMTW!>Mm5iQfRUNmtvNcd^rKNDNq??RKqsf3; z83)tk_xbLn`}=0o-WeSQX|RT#y&u&D#b?SA=0ZU-FC- zp`DI=okDmbjZZ?QJ$57LwwskaY31rSs>cM1iw8#^2I5PFL%Qs4pN5A!-#wK#PHgXv z8@L(?J4>^%Z{q%BPc0Ci0%3V)$xR&=H#WBH&|a43C1Fmjc_X`_AuKOXv0p2rtlm$Q z>4l5b#_my$->JNSfPmveOS9Du&({;FW2GU!GozACSFZa)U3$xKHSp$r2)~>yNLxGv0CJAlO`%{#fsJi zYh7=en+;!0bUS*2{hc2L1+g0%86g7B1n!CQLHW=IRKn!!pWK-xo5&cMZk``(WJnz~ zTuLa4V4xTWC}NNgW%N)J;ZU`ZxnI}&dgjD?!x6`=G3=7XD68efT3lT97OV`3RTzeH zaZz6-CrM6C2YP-rS-r6ipHaSYRRHI5J0N+GKQIFC1i|FrXS=Npt5f7eZWnHeR z;YgEo4cBHF<4*unp*z1BkO$>2AD$2%lZ7s`GIp%*Tbg_^vKj;>CNY?pjZF=IzYA|3 z;EYT6j38e566s5F*ox0RTrTzBdq#HM!k*D=RQQ>I963;R)HVo77`diCY-ztndwNQ2 zA1Y`17F<_C`r?g}6gwR>wc4?bIQdiAD$LMmQRSA0*j=E%0GE30t4Zyg{_T84DQ ztU)5Gp<}5;C1kZF$0aUdawl%Sc$Hak_~(QtmB#Q>h4;6<3yX<0FtUiNh{erL)D4TK zUmvO5DgJsbCUmKo*Pz9ZRo2J|PGDzOf*mOf3snN(*^_5?GNgOHt$ZxsQNwaP3d*;2 zbMpZH_lT;Ng|uK&;|0S+hUC;#D?f*jGL^&Bu6^ep9>1c{c#WbMQ{L1IY&p(1SSEQN zC=EZx#vBVpb1;`kC|xh>rr*8Pw^4YXuecahJ1(s8cp*303e(BjQ-HsVj0u;*lQSIw} zNXgD*m!Jd{XE5Lapc?W92G6^1y)uqJ3zDs`e`suKYKii%S%z79_fm9>md+Lxt6Ceu zJsKXjw;!?SUSnZN2eNpeHrGF2Cn5j>K^?s0bhdt-Qc_Y5jt@ulfOZ*R@~i3^8dZ(2 zWjJo~AHa`2>jJ!!WX2C~F~lgQ$akGRoQ{O%O3gocdr=|QN5DF!E`x?$x&I` zP~O;$(2fAOaMN$HEO}7-^O#xnHop@21&!g#vD|4}{wp0zG++q{IRSd~%J-uxyCNlC zBWJ$gM~mW!x|B_$os*3@woZ@7?Eblj!0i; zXb9~N7cqduF_8jNnXF-x?_38Y_1^e2T~>0ObTwZ}`zz#D}?8J`&9qoJk%$@jJLLu;wp1 zcXuYj;t^l%&O|iC?y?!Fq>41_^F1A1Z38O)Kv-K_b1Nx-kh;B4kkZGLaU-_Aa%>H# zBwvl_-eK1m28wE8q%Pn18TvCz8UkKs^SqtBc}zx%bhZQtXcpkbI8WCj$Q6tn+<%cf|n{)k`A|TJrC^zfA6#@iH2px3bO6g{g4GrDaOg zI`G}4R@A{K|IRiqGBTvx%(#=)owwiL+TC>ptR&k#_YJ7Vh)j#qkKo$qLuab%79yZr zrd+rARUUtxY}^+<|L~5+YM=!&<1DuAgWbH-Q)PVb-op3xUD17eO5}ol<<))_F+Q!Ol3h1R#v&zmIDGknz4qsrO1 zwIUJX_HMtcRyw}?h^Ue~$4prI)i|+DariK=ax>5-Y>!yF2b=6gCv(*|iH0tDJp8e- zd^=gavcCRV=iI`NSoB-hK4W@@r5-VPs74NeeBQl7i8a*g(1mQ_o{ZI*Bqq`G?+25C ztEnEUF2q;odUvFVFSc~{6Q+#gerkLfm3?nwR&8_!fn{y`AGq#TJg%1^-)tfr;0QV zLBakU9txm1(7w7WEY02XSapHQz#tE39md8A*Sl~oF5;fl)g`A58qnl)4P}Ko##~eV ziR6FK$CMc#9|I4zUF5%xsnd?5InzzZvea>SdFhb0D$ig$M_bX*@WuuIvr+&3PJiyW zS7$$f@cjA2>0wiI?K;=wrFo``%~nE!?@tj7f|@mMpRdYUR*!ik(A<4Cx$&~0fvgP- zG=ZN*vE;S{#Ds)+1C@v8f@0Cy@s9@GJ$rn60BO}7)yW0C^YJ&z+1A;Nn6FAME(6dM ztl5fAu{d_jen^*6K9TNvXb4JM0I73Xtx~6|oN_+xNoQ;yxF=C)0k~Fa&4uBr48c7AzSJ*r4e~eMrIzJL4MA0k}%-G&C|I z&qfPm@gm+E>azW2I{H9?lJPoAK|uk7L4t7o*jRGtGwR%&LPF6M35%|CZQkaj0ta8L zwgGR|5p2SfQpFYxW&1l%ncXP%XpG-EVt;(CN245*>72eYMXiik|se|nT%=;rBJU8UPnsS8^35SMIfDohE@=5g~ph*7sj zhy}}$pPUiMw{ia5+e6omKkY=TYnWjkpfWI!!L3beMyhC^>oaO>1!-^I^8!_zINit3 zIyHutSTqtD z%+=)uq}?{LM!D@(TKC>!*e3U`!3*)Ez@6zz?ahsu_@^QT!8qjo_w zZ%x0~m4Q~rk4#4wlz_yM;>Ov#6 z$YrMqk)oGEU@2E<`GAVy1E43FGoQPpSESz;Rl8>efIqa*iNOknyxLd3>1fP>6Bi{>sppC znq8MMXs0UXJ0neQJIcw+G^%=f@+67Dp05pFoVKqq+oxf*%@|}!CTKn9-U?hvD=&8p zrUfe5Yu}i!laqQcJ50B)tUv~T&@a0&P2%jH&R$fUl{se-TlFImg{B z&^~HsAki`xk(SWC)_7U{YW84pDB0F5UuF|;+@0#W>vH6>Y_#?Wt)XSN>{YSl*Ao4X zX)quQpKu!nDZEGJ3!1l9&Ho&Bv9~f~NE4K{n)k+D`FqVE%F5%DQd9(*9!I|2&grVDAF@(0>Lsae{3tL+l< z@@+SeKF{>|+N%eofUM&OjK#p98+UIw&C|;(UWSc4ux(a`Jfy6Qe5pSjh4r>IqKR_p zdSl*}lR@aqb)KzXl6S>nj-MmFW-6nb5phFxGJ$O5i(4J~x#p+rE0EW)WPHX#_OHJ=)@Vr7!IKznQ#$(DPjN^Syhb z1)+hIpK1w+4>wz*r-9o2{lvRAZpo^*0T2XK+I-C|63l9HiuuBetr$sx>kL$)q=cW` z(jSv?BYb``=G$)dIZ5al|0cN`G<4c97ECl=uLopC8b|Hv>)v9qNGK zK(82UrG6A6Q~fL|a(I$cJin}>LfCtQ(r(ob^q?@l4O-QQsSfmY_7^+PzA0p#QM2u^ zG~IotYe|?g!fS#fIrmc!`*f!nE?@r!3y3f2S1IQxd4M}`PiZCLiZ8LE$hDrTuiwIH zC%?Vlq?y_FvimZ_8~$iuvlFfwPqH=$e|K+@7e`s)cHl-B8-inKBBxeUwwk|26{%B;!XGT`!NRoeFF}(pwbi zpI+IKp}77lRjKC6L(;KVi`#&@4*vN5Ju=S;=(lm)LxqMZZ~OR(Mrs*|P6*N@+E$IU z_Spme#pNfWt&KCdEwyp$1h_|?xtmjgVq^>*teow}C6M8vsLcHbQoBE|f-XB3huUI1 zhRcsxccjS`ZB>}&6rFsVr|MHHzO)r>sV+KnIm?6CCyL1<3M-5@3x6JMp4~NLNhLfy zrSX#^G`BczMlk{%fXkdjEz<*Q_Iq=0O$CA4?x4+U!KW8uLOp-huU=Ic0=kLE#~7{FqM}O;D;EdJm0$**1*VERV2!J( zlTqUf?-uq2+}scRc`ru`i}(epU%hid$Y-vHi9UtHNG)FD44z7~!4{JOld!v6Y|W??v5Y1F-3oNLr; zc8g0(`qXRETt`udAVOa(#BhnOcYPlW6&SWzTEf+NWxe^Xkx)plJ;0nYzTS?jy}I_} z!`%LrA3xr&yUaP)12>^0E%A3Kjgq>Rm^Y)3v*A+1!LMzzieJ)WQ`IT&d!>ChqKca~ z9q+YSF5UBg7nh#cMFFN*o-j+A( z2XHy1b%_Q*kdNDz`P4_)H)p8V*Ec?Lz(gi$k!hVNpQlt$ORqA`qVMBWiFa^sok&d*ubPKp7#YB_I$;Met=N zslAhoFgiiA!l2>V)>*f~kx4bSPqJ*Eg2p+y1?9*g$*)U;TM*qioAk0WaBLS>A~set zubDjwnyOM4y+CjDxlp!iWcj>*(ZUMYjoya_7Bk-e3*R90Lw65@=)EGXi zGT+`rCW{gYQqf4ExNh31iCaKYxOe2K&j;KV1acykf?;vWs~F$=z#V~CuO0*Ubu$7s zlJmcq2_`Ws>U3d53|z&r7EL#~s9CtuH6LuhpBKCf9R)6>wasl`At$*=azgSbJtV^? zFrLHI#LCt6apaysP0jlc{C4vv?%UIqgS7G)B_9uW=JT`t7#ON2)?TrbJo{Okmd4k+ z9l&Hl`ZLqkOeC)S`DlaG#%92et72P{Z)$q18_077fey&HBcK}*`O9^t{pqXIv$K~0 zGT7s~w@zFkfm6?ZNlxPg5=w8vT@h)|qFFyIyWyitM8qYk%o6(ga&)aYIq74-c4iL9 zoEs)WmYq2+TRVD$16j>Tf}-xSejdysPl4y-g2Izfc}nkc)w!P8XfA=oFI3cSMP6nU zEaoARW$#%J47%twrGqH1{XZq0by!tt*T$u!1*BU*>5^^`iBUR^bV=8tL%LBKk?uS+ z(%sS$a_DZPOS-;?_nSZFy2fi}&fZV1b>F|W_I|>+fG*2nVWP6R?dsI0pwif3(v&*e zgNZ-0C<)SxHEru3mfW&3>13mfw1{AeVxx0&P~mz>`qUzK{P_dd-d>!SXs-Xc>=$7Q z*YKtGtlASuxde8;2tLQx8kxzCkCQR&x3{bk!mP zXgzuA@F!)%DW1p?MM1_;KEAp|n6}bTlM*F-Zba$r)^LaDRC%$pjw>GynMkL9wOWb3FeiuSpzs~Q|XV?ZO ziu#|9Ld@8;nyO25JPr~zJIJw|vv!26_e%2Mj%@VR8d2+)s7XL#-dzh$C`@d0JS7;| zP{bx^97*jsIC(Nx##Jpj{tTkgC=&!nMX}$!pvWpIAx|8N#0`+59T{Ik9Ac3JXPZ>+ zA4*f%gxZq3;m;4CL@=jN;gTe%Wc~_6lSo&y-_c867Gl((vAJMF1R>lEGcrID@wc?a z=yu#b%nt9E_HVDY8LVecGi&&3j?ctB!_)qlt%U5~eGawL`c!UgMO3fzai28g-OpU4 z)L|J5<8)^s@Bo4Mp?~-G5?vhQ%*hWIw9!R1BvKi77K$y&kdOkwR0FPeq|phsCj={P zx*VH#^Luze04kv*TGcEo*ki05Y%ej&rsWYis|K35wTgfKSD72!>m(@Gm=b9RAX&Q4 zFXy<>JqSI7)rqDR>elGPq#LnF{b>UdXmLYOLR^pq5L@K(hAR--ZDxOrmsg{P`(#|6 z@1jf>|Cibqn=z?@lq6s8HO_sZsoKXtU5@ol!$0=R~zDS$uaoqenlzBAts|tz?kXlT6Tx+3PwwANw6+*|TZ0vJ zC9gj^(3QXqknX_S+11(%u1jZX$TO8laV5Ww`><}`zmo}0)J;bqdc6D=1cFEk5qC5i zWb@H9E3I~ym=T=RMMe5JCUhrv5>l#esGI$2*BL8^#>M-_1t^F*clLyp(J(>iT~NUO z6XdO*UXN$(OAi&KTCQ+}FDviH1XlD++IXRNXx+((n7(bd;+)A{b3nflQcvL95a+h` z`>buTJ0hZjteNtx#4w1A3v$pbJ?F(@U+C_nN1WP$PcR@hVTg6*7RzT~J^O>f=?J1; zRxTx!P76FcM;BnAoVd4uNE1+8oLGT@s5q96c1fo1^E-Of|7=rlo;a9`T3b-5-@9DS6s&RTblbX zDhjUd^0o22=>@7?T0TReAsi(aReg$%J~{10xaB(`oi7BeB-!71B*Xa;4Iq${KP{&< z-9?6OU-{wg$94Te9*L0y>@zF8!@@s6I||3n69S`kVJmlCpZ7QF>gi#X4RLW+;2ei>3D$cn z#PCW~4r<6f%R&pA3aoDw88qbiY=}7U3T37^uBzR(?r^=cg0&yj@`Z-StP!`pq*C0>=l-J)GfoI&@ zYQaq9IgG%F?8TwVsjtVOK?b_)^|c@nng{Dlp%mdZ^aD^TdT9}r`XeIXwt@T} z*F#VK=aO=@h~Z7i9KQFw@+Hk{cu<53lS(s10yC$-RF^^tdLy7v#w zxTm5trv|6g$+s8l|EXkzsJS?OX!pa)!bFzg3ty)Gwdu$$_$47eKHZuTyFmG!!0u)*q69WRW5xLS+txh9vKt;slvlt~%qd z;K(go^c13M)Sp0H9MUsKPrtt(zjm~jSf3q8SNpLhWn1R;o{#{ta$#)(C;G;QR4u=b z5C1_(+?YGnblA=FcJs%yb?ksb?E;kP$Bz*cd(VZ^vN8ap*<9mOiGAdXFV&1v$AL!6 zUfjs#?;P&r)?Yr$9+O#3TVKZq>Fl~=hpc^chUz0`VPj2IcZ+KGL8Z%BSP;#PCR!5l~FyQP8j zfh(DZm_wdR)Y~CC$WBLS5&3F4A2lvl-}+|AV7OPK&A#mGPCn}*0*W4f#JP2;?6|u2 zMSqYHCL~)`=WWolP+4g=#@DpZWk}yG3SIHodvgnGCqQBj)d-|NCFl@gVxAFpV*q2d zrRw*}$}6At_9EWIq0Qcd*xqS?k@KaR0$j>amSLJwII4VJQ(JrE?z&n5_G9vWI4qUe z5B1}+&E(TGd1xtstcA0dv23^iUFO#6(Fj|O%!qfizIZ@oY*mF^FVPP1Q5G^uhkXDE zvP=%%uEGtl;2E6g8SJ?Q&c=W4_|hsMcNzHbjwb}Z*;OD{)Y0-IXmlI6q_{%?)#i#P zcTP%2MXR;$P3XtIj9N`-*)=w;PwQin@SXk~=huxnF6zJPCrws5ay|s5kY|Q&OzuKF zH5jUAZ&;D+MCvoNj&=^n%Vk5dDGH2;wmuYCaaq^){stDQt!*SjrhCM|KsB37;eOsGd{}y=ZJb|6aJO^P*2`u4lj4Xerfm+x^YbRz zl2W5swq>n4zZ{GO1Z-IS8l%NCY82?n<=V*ZoHiXhfC*J`=`nW5-1+8;#YE=9%Ox1t*K62M_YR$^EftA1Nr>1W`yfKKzVgc`pfl`% z$~DW941kA@yen0V21ERlf0yMXcm zjmf-%(Y-|sNC$vf07jHCILT(WG4DPkrz`9HwHR>XjrQ7nNm5dU_f`ut)%lvR=?;TZ zEI9H%V}4c9x1AKBG4X#MDX%hhPfH_hY)ZNL`G;1!Wl2d%>Bu6dChH~8P+)2w+d#$& zlu0|UZ|)Hm&Y11XneaD7VxNW56=h`lfXeqe4*jVH(j`B8ptqbeLVi{?+bf)10dC)< z`?pD*t$IGaK~x`vI^fDvn68}N4<5W-NICb-5O-sK0P>fOrypb3)OY79RS@Xt$UL7T z-{o`m5tAFUeh)F3{e%C9$8_VrVn<6dZ9!hP(C2ql$M^khe#CwVPoOw*;5$&9^0TVx z_x^9^;j^uyXkZ77kAHrT;Dkx)YTu~qE<(k}S6o4OCW?UFZRlXsc33W3)=?!NVZ#hd zC|EtY0n!fuB`Fk%fn%IH9M!!esq({??hAeTbDIC?*MAwGW+%wM27-yxH>o=getv$} zh`U`X`KO3LEQa6d{VeTHM9;s^if29aB!`m?4Kr!$udFo-KW%1t;Z;b`AGM{X5y>+r zn?PL?B(eel=OIZ>&WmXPwCB80H+HKa6I!6${5rUp*k`r|1~fIvFSBX~pHtRtW|PsH zmk@}Psj8SRJ8D~6ZXX~(>V@lUQ-{qyyk09UV~_pX_f3Wl7%jB13Pp3RjsC-jukfM? z`JJv`nTn)IGGf+^A=XBVS+Ms#>jYTNC8a@Fp9i)UHba;wkxciH>(`MU z4u{3B7N>~Y{!xx6gG?|SWHa16B9a=_&<90gC0(f3X)Ph1V)B(&Y zPLG3AgyZ>+5%DBkPrl8dlI5!mTi3p_tNdTqnL4WyiGyo%>|w%ZjoELo^#=do{X6Bh z^F=avxBMtNde7bw~WA^Ca&R1tv6jMoTe)I^IO4paFfUju{94`kQkh z(8Poyw7c{7ZvqgzVP`T#7g(YU4i^b|!AlCqt2?J4hWm}sJGagOchx)nH z8TJhez(G(C%*7hEGZ8a=ykd4Ca}yLmCRK0vulcHzie?J*1Bq;Q`L1y)X4e}-R=k%jI z?9a)UKWsSX6t(9V^-ol<8#n#)d12LJ?{GOwaBUx??@8Go`|aJ0M}UZnBS=lf0HTz{ zPKUJWEacFn3ut2n2>pgEg|tfEYP`IT9y@Wa@w!Dy3VzGttbP8tL#sYDP!Yhi|F`48 z{(-W*qN1{{E=8xp)J>lHEG5;`#69zQo61E~e5=#{RuxAePe*EsXot)%6Bl@%@T+y7 z$=wICPAfyozn6{IOY<_Cn%sX64`-hcEs!L<2sI8hGkZAmz>>xr!a zzgWh`$LAOKJsH{aJNx@<5(eUw>3YB*VOpg^uWqaBJw2t3{jZ@wW6H+6Rx$&tN>5iy zy|$8K+N@zN5@&_f)yV6Wu`fdOgcs@2G@7j`Mvh!_w!izU;$qH;l9iv>T*CZ4r{np4 zbvcXz6kIqpHOcw7h>1gbPCUP-5w#Og0s=nKO8`0K9x3qGXBD$;GiD(tAm3~{!o@lJ z$HweCjlRp%gBz@4caUqZ5wDzzb*QWVwS`fwXFi&aEbLeoN@6~?EAp4D0>j_R0yLC z@nAda$>Jsc`k@Xn1^X&-sHaG6w7RaYsk%KA7nrMD0zC4*kFScEenPh8H3Kxq_MYlRu97jdBR_#mJxU+uEFMcJ*toSZD$>vLwe!#we9F(RnjGK4Etw zhtEZkJgev%8GZOgezi~BK3r5?{kJJtvG+;<`f;*`Tkf;FFp)zqT}w%v-Y)ehfMr}u3{rKI%8ue?yO+} zN-G!JrLHpV0&vFS*+Q=9;54hPW=en7@04UzH0dAT^15V?jmUv?jWRtVD&h}(KD4+P zL^HofMt)Yc{6|~aj5own?O8j5bj`_5u8gWYF_riNvId@`)KW7f=iq@O<)e%C zLZcxC@zLKke9gIdvRECLUej-7WN);hF>)GJl+Zdmvi1j#Yb$(;+RnBR_d|?g22De- zuwJa5-z0&x@KJGVaqk=yk;XhAAz3uBM?%d~z%te&r`$N@1C$MFw0$iuegdHZ?Dl=w>}e|i8$IVw zSpwNdzPMj=%`QtA$ElNi$~E`o=g(^D9|z4#pfLj`U;EtPY+vlI%P!l>w)kYGZpc~n z=a^AHG21#A(F|qN2;QfWOz7<1v(j5@TBujVjOJQ1+cn*rvX+{Dxl6{hv6(2#^Dkxw zDg@YZ)MzVbC}^SYXPn9F+x|6!7^JkUob$^A1-L@lIFfiH#yd8ylGzMAia>k#>v=TeF zR3L0HA#BP{#U}#G-Z9JfS!%fxhz>KnFPMP(r~`FRu#B-)V5#`%g)nPbNPF8jHKaC8 zXy-oN)0wtf4%iRUv0*16kBwfKog zlW+;k_a_^5d(<+u5M>*%#k5Ky4~%kh;lj)m+QC?PM@3shc1Vy^Jm$Fvawc59K@;`%w8>so76=AaAdB3 zmS3S}YUw?i_B@H+WDlzs3Hgrd@aE+47t-S$C%4n4oF6z)_D-Olb^xd0Hdm}IG}ZWW zArSN8SYc(f7?jvlZBJsxD}#zVvjr-gzNqC6IcQ@%V9hM%q8P3aQimU&)BLJH@I@8* zHX-}v`IU~z{sy6+xj07~`hz9M3n9V8H|Z?AWJlBXbC<3OhACVGDjbTTk(n*)wYqNZ zh5OsC%}Bnl(E2S`_V=-1O6@;kA?9SH5&Icp&Bh%{G}7N#Xk}Z6xJ%SzqaF<{EG<`~ z+(kl0k6=JRZ)@{$PK~lmA_)QNQlsT|U6>KdUli!`CA2%%@%UTg4mJsXRs39^L>>H= z(@pGi0xcOum`<3bq=@Aoe03FMcfEj3B6TtKZb|mIU6*3Uu&&DP_fp0mh!Qyj?}Ksx z={?ln&zN9Z*;Y5mtt763R&#_h-Z@Uzsz8IBQUEmY9bwL2{<=_0Rw8d|W!`R(Lnzqk zCN)4T486JH2JU7~PsepY-h_^f=zRHad~$YpWd-+})c2m?Ye}^pXCcV&d_Jx4zAnz4 z?qREf$;VN;e)i006`8^Ug3xZ2x;E>?wC5SH0d|Q=G}~m>a#D)FswYhZAs*^x375_v z-)u5fo#*HJx2X93#jamquPr8-s-;R_<*a1L$KBD)N`1z*LgZY#pgze%T zD=VDqs0<_&WS9eSKkzRryLRDpola_DI%H4UNi*+B8|xCQAP!V16| zOZO`;%aq?l1LF_ z-FlD1L6DoT`oUSxK!sqF*k7qLf36%D&r{RW=*W3^FQOyBMg9Bkj+F%%Kc%HS)rNIQ z%c-|F&s4eY#bYXyI1u9oOun1Yj;zv6gAkEJq@9KLtLg*Zm0g*LAysz}H+N!CVpNoc z$d@|D(e_V&A`pCzhN9P9n`b{Z+6$xs`GAPniXSqIeQxrz|K>dopr|w2d;&TZ5j#f$ zM6yh^q`GK`wx&U8(QBf`&)(iB(kf>{`R&NRCz}%PN6eT)6F9an-CG#^qLIWaz460` zhjD>f3=(w^9}5XJkx)^+A&Yce?Rt_C-PUUzX{3$Y#VMG)xza{I(R4Vu`upi}yRDtN)(8D@JOR@BtVo3@D0rhnN*IRI3mjfI-8c^8H zpwI$Br4jU%#OmbV9GT!!(}mYmPzrkNXR7T?h>wmAPfa>iybv3tBBkzF2zxh{i`zf> z5L~C8z<=R>W$Y>G+WHTf7ihrIqD?=|r{Bm0Z464Pm=M3I?~~)4`QYY84GLWwM8$x7 zQN{Xw02%Znuqmwo`4T9pVn`j{Kj!_hV7*-kPcG0$$ffeT^3QCmIPO|M$tm95^Q%zq zL#B`YijU@Me|4;a=>O`QR6y{x77;P=%mP;J1|kT=`Io&g6%&WtFOp&HZ8bHZR6W!G zO(92pHP$;rgu?WPKd<=|?1%7}M2(V;|IPB8Oe|d#4dtSdaY^{ewmGG6_}&*K7=Zd) z$8_@)K=Fg00yurd^ftKEXoFtGeZXJBs%&ZLFtY&GE+JO8#p_5m6fb_&^JZpAl|Q!e z(*BqD=VNk9>h7__W|gxx#KE{e8ysrh<`7fvnoe+ZW=FaN(SoGaafJN0ckEj@wAtOau4Lp^E4UAw~2L>|abTk-L9*Kc23l zX6hK3cF&|T*S;jfe*X*$+c}n3-fTN{dW}^Y&T}V*Lr1s1aP@s26sHCG`{RKsvSjC! z;9#whbZ>t(hRn^UCL|SQxZzNv6ti6J!b`-=ioF!)?KZ-psn7HMs2j}G z745h=6{bnaC_ptK9e`KBeid-Kd7VAhTV9Ztp02lW_g!begiGXcfxD-#I_5=cJL*Jq; zau&0R!0(pk88&?3(CRXDngoC9A{Sf*_<%{>qYB#)fMg8KZfHR+zOKu0jg$m@YE@Vtfcs?kp5#%q+H%G+l*o};}@mr~PD$#2-TV>%qA45$8@0X5Y#zTxP%2&%b}{z~$HV zr2`dQK+*4k*5hN8af`ZSz%spl6oqeV>TADBwrE^)Cb?4Dn0lyHR=*EBwkJ|^-9Qp_ z{9;mdz~C=zP1(;GQLN_-3hGLlLt|rAJUnjxgQT9a;bGVK zQO0C%eECy>;|{o+$L8U(Zb}?{sn~FyuhPb>tRvMe0if(2PiD&j;C0d*flAt1o4$8ffQn1 zKPD#$pJl ze_gD)HngTxwN{w_RjuH&&w|R`~1%ve)$a<+-0n693LAmk$2R0nQNWta)WC}(_af@(; z%pZVmgp)sTpLo!iv=32HQO4EtXu*6|8N^yHC(J9N%kKZK_k{13sCSRA?ItL0t6!k? z$=^XyLKRkAF}Sq8(qAy#J>x>qRoUg2s2}Q-@WqkDv0rrQ14SqA;5>jv6Lh=ac_Ku8 zjG{Do!Ys*5F8huZ$0QtEl-cc0oq3DNs3oTQKUdFg1>tJtdA2hHtc2ve2)}RrEqGQf z%_Ly|Jr6aZDk|6m$9V*oXnpwXjHWCE0?a`?Nl-C3K~Apfx4vz*6|FQ3K9sf)+FdEG z)H{|St)%=8g7U6H(qbsje`=?>w@QNW9F~o84;9fD&$b7L{1?S^uMFTU1a%uFkCPP*KGKlG){gFnGo$?566 zX4TwWT!a08Nx)_+EBkM99ash+J|v~2WUwZ|{`MR}Y8`zmVXEQ!CZ+w89H)G8z!bSU z9%6u4@T5ZZYTHqBXN}b*8f))5f!Eo*lKt*K(wE=T;kPnz`Kcy&?v&U+3Ym(1q!szk zCiukqr_Z!f3hv2|`SNmeXhE%=o`(c^atKKX%`~oD_?4HKOkXc46;vHoM2pOWv7aG;Zv*=$Hjo2`(Tz`UKQWbo@On>@8op9Dd#kwJo=|327Z$7)Y1OxD0UBTGIBqH{Y z11l@->T%tU1+dbboRBJ}`&Py{wdmwPYYop`@~B@Vg0eNSw2HF2iv|j~s(}k@3W{zvKy~%2z`*VqTUSub-zl6jU^0Uh3^pOq_dtsi zGF3>F)MwrH=v-=`-{lB1!ZHz}C3%cuXJE)SF?;PYB3X3rMMci{XZ5!Crv~hhd?&`J zB(rAA_jk-PdLwWeAKv>FSmGesNm|vai+{_>IUIlXj$Vs{^*3n8nTA;x{Z>rOKZnuA z*4Btb3wW8SsWgb4uYkUmr&g4rm@1MQOVa&b^vau;dsjQea*N9_Ad$;Z5-;xeL%!cf z9E)_glHT;O2StnhO&65`p)cB9UJpxpE{oe)h4%Usf zW90cuB`r-Na)FOAUz4L*p?QDbT_f+2nl?1&r&%eKTV61gH!oXk` zM1X1s(7oX=IN$bn#VC(aS=+^rwP8cjoKNWrf2Ra4py?K&i9%VDhRJ3l=(#cRx#n`t zkN777XJ^&Fj8&REZ%D)io`UUfQfcAD94n5EB;xL0O}JDNui&RvUlSKND~}%g|NGrl zfZ4*1l1krRacGPpMYksogDWzp8<~~o2gotRd^o}GW9M1?(&Nc5XX&wlFF7_lDQ&Qf z&?2_ntzU83P(B}JnF9L4y&~Dq%FT@fQ6b3H8UZELg+z_J) zo(?IlNVp}ey`s9PeHaSz)GJ3Oi&MGTOI)c|FpU0G8SXbc$y2P#;(5sl?R0DI_Aj5e7ucWA z&<07Gg)s=BC>^g~wAce;rtfiTv*@NcE#_x({b)4l zfdR=Dcd)!%O}&Bx(fsER-a#g5^0+j$a4dYtG;DYz|0A>k%nIBCJA6j?v`Y^fAP71@q5w;S+@y?+xdB2{6PbcPi1)x z2XADr+B8~CrhPNb#&|mw@IBsn_?XQadRRVU{anI8>*l%66~BL-x>}IGd4%c{LSK+l zrT%x2ImR}q0G6J=@y~!(@~do&Qe_DJO6-40_U>`DwS-SwKt~T0WPA$_7Bnk>J<)TP zUt))qzvfMl#SivCal|sCA0FP_+}wam3U(wR4${6S zc++cZC&TFej(_}0hacFmYf(LEC67PVO|R##i;-gP8+d%KC{9QN3X-bhAmj*;H6=fT ztCibjQ=juy2C^kBPrP8l!9P_j{0ebExS2n=em<^nr*VE*!iWKl4%OuOR5>S@EFU9J zKcktr>koVhSK;fOkVPmU76IP$cNY{-+J6N|s@7l9a3d4!oJuyJ`hB3scSaT_r6QG{ zb^*0lYIV3XJPHp7*3oD9ZWF9qGx(~c;pPn73j*23(RJRG13zOsT=s)~Y4%VB8s- zx{%lFr>zu#$}3X;4%&>(?S`?crDU2Kdq*l;1iG_^6kCE8k>iVtsc~eexL5ZG04ew> z&qSr`aoGPUO`_wyl1A2&L4Ljo_RQBL=WC95nuBRHi^-jPbtm}s048s6&b#tGpRK?n z-|0-E_Syb^hRw(F-(;;a7DXrirSi%U6bPg}jDVrInOcA88QV}PjI)mUdZG%51y3Dq z*?)6`B?bBnfYg^SHQfuY1j>(1x&IHoe#4DbDAus7&^f&}Q zYk!lUQmg{l;{I6qz7X%?_f@GJA+CeT zWi~bJ1@_oybx3?w(CmJObs%tnK|6Awin@zLiR0#h*1@PZs6T&#OFKzNbG=}Fy>NHa z^VF;gGV&+wB=BZ-# zbK4najP43>|Z67Q|)`q<39!isZuZZlVY8tl(6#(nKO%bn`; zcjNb@(5qi{^~5Opd@Bw3|M}$y`cctK9=_1f_%WYBN(g;Mjr)USNGVP0ciB)E#EjP4 zc@mci0m1#c$Cmjs-GdO+Tb|R+SkYI`LAcR$)5jIITLRZES(YicRWDc#wLn=P>QJgQRn~O#DP9-n`)W&pgp|UuBf^Zx0R!ZP@w5R8&+SR3q@Tky419a zQ3YKh5q7Rsopsl-Vdm3R6)*;FB*uP6PymIlz-71(lwudQ_vyXSbUr2aVUQc3j| zGhfzK&;Gt)Njdt$qmq@=T1nixcs`!ilUX-?B;HXsK~Rl8jwV??AkE`};uxn{P`@qr zd-;(sg@qY3Ih6Vfba_l})#Y)wc??t~m=?lQSWM@iXER0&s_a-_H3 z8KE)Btk;V|6fdHN7XnNBCqcM>n@WQ?rq@AQ2dV^p+Nm>_qq|o-%&Lv6XPt`d5r>-8 ztr-bWVBP-1(7u3T>!Ir&-kVz?^ye+Z`aW{A5h(%(7*JSg$TV-KOy0TmI$4Gi>al57 zxpr=7yB;6+R`FI95r8)w7NDTb0@ezH8OurovHDgwm;l^%Na1I3qV zu}KB;BMJUS!BQemm+{dhs71hdfduF2H^T-nlh*rB|B*bM*BF351uPwpg6aTrk#m?k zt0FB;JJ$OPP2!Hk_6Ht2&$y$izQ@zF)ZvwyA*(B-bmo)1o-S=`?ha^UrgP^9KX z|BU(PfWSdU3<7O;2EXT~@kp~ay_sfB_*TACSR9ta{5OSlgjZpfyOR^Ujy5(;zAXw` zy?TZ^cDp$-#fg=p6B{5OVyUH@6z;UHqVZngzY7d_05Q8+<8_R4OT?x1YgMA`vZ+YF zf_M$ll7@kU4qw|@^a?Ka=93Q}zLJ3B3NTAMBT?m#rSmqKu0rYJhMRLr125^g6WQpd zBX*p_oSA!iZ^!_*GGM|PVMDXN$b${+pelJj$?PVf{4L=MGcMtJ2!s%9x!;i?pz2wk zJ`i`s)$_!owLz2$)Nnj+;<(Kud@|TKda(E@tFoExou#&u2b|>sk3%f8A?yvUq-cnd zydw=V4YY?Z;XGQ}x)ReaU9rPIEK}?hVQ?Ii@g07x1Zko4t}0e}_+Z(#)cbqtv=U(3#zR+K~8x^U{>q$<=>XUKdA&iRL74c?Q zIIn8Hd8?2&U*2-h)NhuTX*)by&d#3No&Up2(up?q)k}2pO3i;jB3o1DN^TL08|bgq zEZ;H9^>^8c__O(AvasFGkh}f8Q2N&IM$(Xr%1uqS(^hIkDnNA#9H`;4fsjVM znA1wLqXMx#{f$sm${H~a&4Bb_0=Cf zU};cml%62WlNoGXE%TH>bx|Ob#~!USl%CweLT*MG9HJ8qjQjePEk-{Z*dK$$>booq ztkaetgOO0n0rYaNa*K7O|6Z@;+T+Wf6zU0>i@NvfNDo5-E(lU;#{XsewuA3 z);+)tXA?E6)+9>H7$KOl3JvA4VbgqskldHUqMjE|lW}0I2eu$U<&qwrtK$D#(@z^U zs|F~6D`5hod5gi-X|B9}((-VAZG_a|XGcY`31Qcvh_f@FIkPQCeIeUrw>>zmo8WKVmhycwO!aDlOT)A91ZQ;lb*J1 zNGngvw)|g5h55OtH!sn-b_in2{>yGthCm3ivj`$SJy5}i94Mj)e2z9VBLU*Q=EBmj zAF!$)X+J?rVJ(JKAlY^9(%d>y5=k6?(wV>H_2zj}!(|)la@fC+2M=%Amoo=dSOYuG zpbIY$P(`P^L6sE&1O$I;XV@FI6A&pq*_h>NR#M{bzWeAKjv=(E0bOC=Jl{(+rTHLvp8%LlZJXi{(;aS=*mEN-&Juq4r{ zqt(fSGl3g`J3CVHywx=M38>$ZR9J>y{era*Mz?R)f}eGBNn3mg7CM^=DfN?ehL1#M zZk93)W8%Ky)noLoJ%nE!f2>d%>lA?46pLX9%)%Vu&=pg29Hv`f- znmeIK0P~(vT&>=}z)`YKl_9^;s`MmEPpPJfIJDQ1n3s;-mR)Y^sjn8?pu%I~$4Zi4 z6V16uxprzO{?yTq|6O|v8Yt~JTWOpIg1OL}mg9CLqfP#J)?rSy*5~sNf_0I2P}pjh zmOSjZhew`*5gOu5d&09L?}U@!wZKu&O4Ol}XZ3|#Un)C9rtwS`s>7*p#@n$>zgDGj z3&MTk5v8i1oPD5=3TV9Xa#4u}`9&yx#q@N|S2gDJRsT_uUlaSMJ<>g9Kw{49;^_LA!sjiEU+Kig3PdMf1xQ-n@+$9jl*o=O?>(jl7;c4PsnB z^0Mzzh0~l3pO-%Mi_q$9bzXSQRaYW)YiVPa7ShPbU~)il|8Y08Khj6m)z?%52Ye|2 z;c4Pi`}6hH8Rt-}O2zufIq|H5bT`HRg_ zF-z*o(a;c+-(3Qh#mPB29$p&A;q?!8ar=X}(JWK)>6P4K z;A6KJXDwmMU_`LL$S9?!Z*2sl?fe#VH5oKYdIKNTa~F3KrJAD}rI-fC_S3uk0sH43 z7JI+eef#CG#e8DDNKf2?Osgvvc{EMm*vG*(v;6pFDu8584xj~@T7h(Ba9VcjqZQgbUta_+;^Lu9eX|NAJ+$5X z`{@0Au2e1?$5zExqpppG{GIJzhH&^OU4fPw^f+ZD#i>D^Zad=o%=5_ec-gNVMSt

o3XaPHmA(J;IWBBUmTc_e0lGqFh)CgMVS!gOBc5YI#Kogv& zm*80PTn?;2@u9jx>M*!#EG`b_qES#&Q73F3j#@m7dA{ko4$2w;4Q{)tYU;_yg5N+F z%bCJV-kFqu9->u<2ijV};6NP;ULqJU2>_)>mRcx;hryGqMe-^BRIR35ev=J94RQZ{ z+}u0yi3j>LJ&?&9pB-!ej4xZ-UM&Va6bKo~P{dL1zKw4Cp7}o@IO}ncgz`wB=+(!{ z(*te-+3>px?%Le+ovF zMn-R=E|iOui81X+EB0glfBC9Xv_=o-a^YUv1JCU4+U756L@WW$%3v|Rs`eM1xisj9 zj7~IRF?rSV{cb;RY4G_vq3fOc)-7(|+m7k>(xSN|6RzY-Q!Yz#^+U~O4K1cBqCa)R zXyICV^BcyGSA@{U6Wof24D!ky&ohpUv?0emm06Gd6~Ep%^KqV)UkeFl3B2VBN9ObM zv(Lo6w@Y$CZ&6Pl?)Z;OCMjDQ+A2{KtWE5ble+BFFKTUvs@&I-tMAVH9^%i`Jk@Ao z2Q}q6J{-Z|>}0krRcxI%7Uz4s#!}(e>rwFzi-9vd6>(Wu8FkMc-pF&@2ukP$bNnitN^kX@ByLq z=CZxNL6mbh+1J+Z1k=_jFwwGSC%#n+O7oc-KtAxSxJR8~13&C}bJz@hZ0`quT=-x4t^Yva+zHW!|5c`_+$N4Mm zUr&?jdZ}06j?4qV6q40RQ- zK>i2Vyf&@Q48FSHcb0~9owp4QnrE|TR4dVE9Br2cLmX5s%erqxXHz$UP5t0#AgKjy z{_^?0E|o#-{t6yPjhfc~>tM(%=sJP~e1rAh$CG8`(KdU3`Ch}H?VcK)*P7$BD-dwK z>nC<;&%9P!d|*d>O!0j?p2KI#K|co{5#0glL!LMNN&TymG%yI2)lZ~Ah_ejWtXgYQl@*KdyXc=b zqkcMGrjOe8E|#S|Zhc<3pK=+zY%16m$;!yO7owSk$HN+{u?4NvuSIq;mvz zJp3@$%?6(_8~SC-uVezoWvyr)nsGJMmzm*(`;!LtInfrapTUYHY;g9>>Guc+_tekzO1cjUgisT$>rYs+MCNm%?5B^x%0Df>=q2zs@n8yUA0+ofNB!thS?8Zr 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%0DS9a=RgnN6rTFa8E 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%0DaQ0Bf%uBaG?Ed6*e1f0Ft1M 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 literal 0 HcmV?d00001 diff --git a/assets/img/spn-feature-carousel/easily-control-your-privacy.png b/assets/img/spn-feature-carousel/easily-control-your-privacy.png new file mode 100644 index 0000000000000000000000000000000000000000..0d386b5f1490d79fe79b99aa87544fc7ace1b6c1 GIT binary patch literal 72739 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╱ZtV$4*K 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 literal 0 HcmV?d00001 diff --git a/assets/img/spn-feature-carousel/multiple-identities-for-each-app.png b/assets/img/spn-feature-carousel/multiple-identities-for-each-app.png new file mode 100644 index 0000000000000000000000000000000000000000..b5082f73f85845502a506c5e1aa061fa007bd80e GIT binary patch literal 49252 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 {A(p#ml<^0AWin%ir9=2%CI7i!W7Y(x-JhOj5 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Π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 Or>dl-SS@cJ^uGYZHn#cz literal 0 HcmV?d00001 diff --git a/assets/img/spn-login.png b/assets/img/spn-login.png new file mode 100644 index 0000000000000000000000000000000000000000..4a5541fba8f1c5da6802307044453cd5337cce11 GIT binary patch literal 89031 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 z�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 Vm3_ \ No newline at end of file diff --git a/assets/world-50m.json b/assets/world-50m.json new file mode 100644 index 00000000..4e59231a --- /dev/null +++ b/assets/world-50m.json @@ -0,0 +1 @@ +{"type":"Topology","objects":{"land":{"type":"MultiPolygon","arcs":[[[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]],[[60]],[[61]],[[62,63]],[[64,65,66]],[[67]],[[68,69,70,71,72]],[[73,74,75,76,77]],[[78]],[[79]],[[80]],[[81]],[[82]],[[83]],[[84]],[[85]],[[86]],[[87]],[[88]],[[89]],[[90]],[[91]],[[92]],[[93]],[[94]],[[95]],[[96]],[[97]],[[98,99]],[[100]],[[101]],[[102]],[[103]],[[104]],[[105]],[[106]],[[107]],[[108]],[[109]],[[110]],[[111]],[[112]],[[113]],[[114]],[[115,116]],[[117]],[[118]],[[119]],[[120]],[[121]],[[122]],[[123]],[[124]],[[125]],[[126]],[[127]],[[128]],[[129]],[[130]],[[131]],[[132]],[[133],[134]],[[135]],[[136]],[[137]],[[138]],[[139]],[[140]],[[141]],[[142]],[[143]],[[144]],[[145]],[[146]],[[147]],[[148]],[[149]],[[150]],[[151]],[[152]],[[153]],[[154]],[[155]],[[156]],[[157]],[[158]],[[159]],[[160]],[[161]],[[162]],[[163]],[[164]],[[165]],[[166]],[[167]],[[168]],[[169]],[[170]],[[171]],[[172]],[[173]],[[174]],[[175]],[[176]],[[177]],[[178]],[[179]],[[180]],[[181]],[[182]],[[183]],[[184]],[[185]],[[186]],[[187]],[[188]],[[189]],[[190]],[[191]],[[192]],[[193]],[[194]],[[195]],[[196]],[[197]],[[198]],[[199]],[[200]],[[201]],[[202]],[[203]],[[204]],[[205]],[[206,207,208,209,210,211,212]],[[213]],[[214]],[[215]],[[216]],[[217]],[[218]],[[219]],[[220]],[[221]],[[222]],[[223]],[[224]],[[225]],[[226]],[[227]],[[228]],[[229,230,231,232]],[[233],[234]],[[235]],[[236]],[[237]],[[238]],[[239]],[[240]],[[241]],[[242]],[[243]],[[244]],[[245]],[[246]],[[247]],[[248]],[[249]],[[250]],[[251]],[[252]],[[253]],[[254]],[[255]],[[256]],[[257]],[[258]],[[259]],[[260,261,262,263,264,265,266]],[[267]],[[268,269,270,271]],[[272]],[[273]],[[274,275,276,277,278,279,280,281,282,283]],[[284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306]],[[307]],[[308,309]],[[310]],[[311]],[[312]],[[313]],[[314]],[[315]],[[316]],[[317]],[[318]],[[319]],[[320]],[[321]],[[322]],[[323,324]],[[325]],[[326]],[[327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946],[947],[948],[949],[950],[951],[952,953,954,955],[956],[957,958],[959],[960],[961,962,963,964,965,966],[967],[968],[969],[970],[971],[972],[973],[974],[975,976],[977],[978],[979],[980],[981],[982],[983,984,985,986],[987],[988],[989],[990],[991],[992],[993],[994],[995],[996],[997],[998],[999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014],[1015],[1016],[1017],[1018],[1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029],[1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094],[1095],[1096],[1097],[1098],[1099],[1100],[1101],[1102],[1103],[1104],[1105],[1106],[1107],[1108],[1109],[1110],[1111],[1112],[1113],[1114],[1115],[1116],[1117],[1118],[1119],[1120],[1121],[1122],[1123],[1124],[1125],[1126],[1127],[1128],[1129],[1130],[1131],[1132],[1133],[1134],[1135],[1136],[1137],[1138],[1139],[1140],[1141]],[[1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272],[1273,1274,1275,1276,1277],[1278,1279,1280,1281,1282],[1283,1284,1285,1286,1287],[1288],[1289],[1290,1291,1292,1293],[1294,1295,1296,1297,1298,1299],[1300],[1301,1302,1303,1304],[1305],[1306,1307,1308,1309,1310,1311,1312,1313],[1314],[1315,1316,1317],[1318],[1319],[1320],[1321],[1322],[1323],[1324],[1325],[1326],[1327],[1328],[1329],[1330],[1331],[1332],[1333],[1334],[1335],[1336],[1337],[1338],[1339],[1340],[1341],[1342],[1343],[1344],[1345],[1346],[1347],[1348],[1349],[1350],[1351,1352,1353,1354],[1355],[1356],[1357],[1358],[1359],[1360,1361,1362,1363],[1364],[1365],[1366],[1367],[1368],[1369,1370,1371,1372,1373,1374,1375,1376],[1377],[1378],[1379,1380,1381,1382],[1383,1384,1385,1386],[1387],[1388],[1389],[1390],[1391],[1392],[1393],[1394],[1395],[1396,1397,1398,1399],[1400],[1401],[1402],[1403],[1404],[1405],[1406],[1407],[1408],[1409],[1410],[1411],[1412],[1413],[1414],[1415],[1416],[1417],[1418],[1419],[1420],[1421],[1422],[1423],[1424],[1425],[1426],[1427],[1428]],[[1429]],[[1430]],[[1431]],[[1432]],[[1433]],[[1434]],[[1435]],[[1436]],[[1437]],[[1438]],[[1439]],[[1440]],[[1441]],[[1442]],[[1443]],[[1444]],[[1445]],[[1446]],[[1447]],[[1448,1449]],[[1450]],[[1451]],[[1452]],[[1453]],[[1454]],[[1455]],[[1456,1457,1458]],[[1459]],[[1460]],[[1461]],[[1462]],[[1463]],[[1464]],[[1465]],[[1466]],[[1467]],[[1468]],[[1469]],[[1470]],[[1471],[1472]],[[1473,1474,1475,1476,1477,1478]],[[1479]],[[1480,1481,1482,1483,1484,1485,1486,1487,1488,1489]],[[1490,1491,1492,1493,1494,1495,1496,1497]],[[1498]],[[1499]],[[1500,1501]],[[1502]],[[1503,1504,1505]],[[1506]],[[1507,1508]],[[1509,1510,1511,1512]],[[1513,1514,1515,1516]],[[1517,1518,1519,1520]],[[1521,1522,1523]],[[1524,1525,1526]],[[1527,1528,1529,1530,1531,1532,1533]],[[1534]],[[1535]],[[1536,1537,1538,1539,1540]],[[1541]],[[1542]],[[1543]],[[1544,1545]],[[1546]],[[1547,1548,1549,1550,1551,1552]],[[1553]],[[1554]],[[1555,1556,1557,1558,1559,1560,1561,1562]],[[1563,1564,1565]],[[1566,1567,1568]],[[1569]],[[1570,1571,1572]],[[1573,1574,1575]],[[1576]],[[1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587]],[[1588]],[[1589,1590,1591,1592,1593,1594]],[[1595,1596,1597,1598,1599]],[[1600]],[[1601,1602,1603]],[[1604,1605,1606]],[[1607]],[[1608]],[[1609]],[[1610]],[[1611]],[[1612]],[[1613,1614,1615,1616]],[[1617,1618,1619,1620,1621]],[[1622]],[[1623,1624,1625]],[[1626,1627,1628]],[[1629]],[[1630,1631,1632]],[[1633]],[[1634,1635,1636]],[[1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664]],[[1665]],[[1666]],[[1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687,1688]],[[1689]],[[1690,1691,1692,1693,1694,1695,1696,1697,1698,1699,1700,1701,1702]],[[1703,1704,1705]],[[1706,1707,1708]],[[1709]],[[1710,1711,1712,1713,1714,1715,1716,1717,1718,1719,1720]],[[1721]],[[1722,1723,1724,1725,1726,1727,1728,1729,1730,1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748,1749,1750,1751,1752,1753,1754,1755,1756,1757,1758,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791]],[[1792,1793,1794,1795,1796]],[[1797,1798,1799]],[[1800,1801]],[[1802,1803,1804,1805]],[[1806,1807]],[[1808]],[[1809]],[[1810]],[[1811,1812,1813,1814]],[[1815,1816,1817]],[[1818,1819]],[[1820,1821,1822,1823]],[[1824,1825,1826,1827,1828,1829,1830]],[[1831,1832,1833]],[[1834]],[[1835,1836,1837,1838,1839,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851]],[[1852]],[[1853]],[[1854,1855,1856,1857]],[[1858,1859,1860,1861,1862,1863,1864,1865]],[[1866,1867]],[[1868,1869,1870]],[[1871,1872]],[[1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,1886,1887,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990],[1991],[1992]],[[1993]],[[1994,1995]],[[1996]],[[1997,1998,1999,2000]],[[2001]],[[2002]],[[2003]],[[2004]],[[2005,2006,2007,2008,2009,2010,2011]],[[2012,2013]],[[2014]],[[2015,2016]],[[2017]],[[2018]],[[2019]],[[2020]],[[2021]],[[2022]],[[2023]],[[2024]],[[2025]],[[2026]],[[2027,2028,2029,2030,2031,2032,2033,2034,2035,2036,2037,2038,2039,2040,2041,2042,2043,2044,2045,2046,2047,2048,2049,2050,2051,2052,2053,2054,2055,2056,2057,2058,2059,2060,2061,2062,2063,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073]],[[2074,2075,2076,2077,2078,2079,2080,2081,2082,2083,2084,2085,2086,2087,2088,2089,2090]],[[2091,2092,2093,2094,2095,2096,2097,2098,2099,2100,2101,2102,2103,2104,2105,2106,2107,2108,2109,2110,2111,2112]],[[2113]],[[2114]],[[2115,2116,2117,2118]],[[2119]],[[2120,2121,2122,2123,2124,2125,2126,2127,2128,2129,2130]],[[2131,2132,2133,2134,2135,2136,2137,2138]],[[2139]],[[2140]],[[2141]],[[2142,2143,2144,2145,2146,2147,2148,2149,2150,2151,2152,2153,2154,2155,2156]],[[2157,2158,2159,2160,2161,2162,2163,2164,2165,2166,2167,2168,2169,2170,2171,2172,2173,2174,2175,2176,2177,2178,2179,2180,2181,2182,2183,2184,2185,2186,2187,2188,2189,2190,2191,2192,2193,2194,2195,2196],[2197]],[[2198]],[[2199]],[[2200]],[[2201,2202,2203,2204]],[[2205]],[[2206]],[[2207,2208]],[[2209]],[[2210]],[[2211]],[[2212,2213,2214]],[[2215,2216,2217,2218]],[[2219,2220,2221,2222,2223]],[[2224]],[[2225]],[[2226]],[[2227]]]},"countries":{"type":"GeometryCollection","geometries":[{"type":"Polygon","properties":{"admin":"Afghanistan","name":"Afghanistan","postal":"AF","pop_est":28400000,"iso_a2":"AF","iso_a3":"AFG"},"id":4,"arcs":[[2228,2229,2230,2231,2232,2233,2234]]},{"type":"MultiPolygon","properties":{"admin":"Angola","name":"Angola","postal":"AO","pop_est":12799293,"iso_a2":"AO","iso_a3":"AGO"},"id":24,"arcs":[[[2235,2236,1193,2237]],[[1195,2238,2239]]]},{"type":"Polygon","properties":{"admin":"Albania","name":"Albania","postal":"AL","pop_est":3639453,"iso_a2":"AL","iso_a3":"ALB"},"id":8,"arcs":[[2240,2241,2242,1236,2243,2244,1385,2245,2246]]},{"type":"Polygon","properties":{"admin":"United Arab Emirates","name":"United Arab Emirates","postal":"AE","pop_est":4798491,"iso_a2":"AE","iso_a3":"ARE"},"id":784,"arcs":[[1176,2247,2248,1174,2249]]},{"type":"MultiPolygon","properties":{"admin":"Argentina","name":"Argentina","postal":"AR","pop_est":40913584,"iso_a2":"AR","iso_a3":"ARG"},"id":32,"arcs":[[[31]],[[2250,115]],[[2251,2252,2253,670,2254,2255]]]},{"type":"Polygon","properties":{"admin":"Armenia","name":"Armenia","postal":"ARM","pop_est":2967004,"iso_a2":"AM","iso_a3":"ARM"},"id":51,"arcs":[[2256,2257,2258,2259,2260]]},{"type":"MultiPolygon","properties":{"admin":"Antarctica","name":"Antarctica","postal":"AQ","pop_est":3802,"iso_a2":"AQ","iso_a3":"ATA"},"id":10,"arcs":[[[2]],[[7]],[[8]],[[6]],[[5]],[[9]],[[11]],[[3]],[[10]],[[4]],[[0]],[[1]],[[12]],[[16]],[[17]],[[15]],[[14]],[[13]],[[19]],[[20]],[[18]],[[21]],[[22]],[[34]],[[33]],[[36]],[[37]],[[38]],[[35]],[[39]],[[40]],[[41]],[[42]],[[43]],[[32]],[[44]],[[23]],[[25]],[[24]]]},{"type":"Polygon","properties":{"admin":"French Southern and Antarctic Lands","name":"Fr. S. Antarctic Lands","postal":"TF","pop_est":140,"iso_a2":"TF","iso_a3":"ATF"},"id":260,"arcs":[[119]]},{"type":"MultiPolygon","properties":{"admin":"Australia","name":"Australia","postal":"AU","pop_est":21262641,"iso_a2":"AU","iso_a3":"AUS"},"id":36,"arcs":[[[132]],[[127]],[[128]],[[129]],[[135]],[[136]],[[148]],[[239]],[[242]],[[243]],[[233]]]},{"type":"Polygon","properties":{"admin":"Austria","name":"Austria","postal":"A","pop_est":8210281,"iso_a2":"AT","iso_a3":"AUT"},"id":40,"arcs":[[2261,2262,2263,2264,2265,2266,2267,1363,2268,2269,2270]]},{"type":"MultiPolygon","properties":{"admin":"Azerbaijan","name":"Azerbaijan","postal":"AZ","pop_est":8238672,"iso_a2":"AZ","iso_a3":"AZE"},"id":31,"arcs":[[[2271,2272,-2258]],[[1274,2273,-2261,2274,2275]]]},{"type":"Polygon","properties":{"admin":"Burundi","name":"Burundi","postal":"BI","pop_est":8988091,"iso_a2":"BI","iso_a3":"BDI"},"id":108,"arcs":[[2276,2277,1309,2278,2279,2280]]},{"type":"Polygon","properties":{"admin":"Belgium","name":"Belgium","postal":"B","pop_est":10414336,"iso_a2":"BE","iso_a3":"BEL"},"id":56,"arcs":[[2281,2282,2283,2284,2285,1249,2286,2287]]},{"type":"Polygon","properties":{"admin":"Benin","name":"Benin","postal":"BJ","pop_est":8791832,"iso_a2":"BJ","iso_a3":"BEN"},"id":204,"arcs":[[2288,1201,2289,2290,2291]]},{"type":"Polygon","properties":{"admin":"Burkina Faso","name":"Burkina Faso","postal":"BF","pop_est":15746232,"iso_a2":"BF","iso_a3":"BFA"},"id":854,"arcs":[[2292,-2291,2293,2294,2295,2296]]},{"type":"MultiPolygon","properties":{"admin":"Bangladesh","name":"Bangladesh","postal":"BD","pop_est":156050883,"iso_a2":"BD","iso_a3":"BGD"},"id":50,"arcs":[[[1430]],[[2297,1164,2298]]]},{"type":"Polygon","properties":{"admin":"Bulgaria","name":"Bulgaria","postal":"BG","pop_est":7204687,"iso_a2":"BG","iso_a3":"BGR"},"id":100,"arcs":[[1233,2299,2300,2301,2302,2303]]},{"type":"MultiPolygon","properties":{"admin":"The Bahamas","name":"Bahamas","postal":"BS","pop_est":309156,"iso_a2":"BS","iso_a3":"BHS"},"id":44,"arcs":[[[67]],[[1429]],[[1431]],[[1437]],[[1434]],[[1433]]]},{"type":"Polygon","properties":{"admin":"Bosnia and Herzegovina","name":"Bosnia and Herz.","postal":"BiH","pop_est":4613414,"iso_a2":"BA","iso_a3":"BIH"},"id":70,"arcs":[[2304,2305,2306,1239,2307]]},{"type":"Polygon","properties":{"admin":"Belarus","name":"Belarus","postal":"BY","pop_est":9648533,"iso_a2":"BY","iso_a3":"BLR"},"id":112,"arcs":[[2308,2309,2310,2311,2312]]},{"type":"Polygon","properties":{"admin":"Belize","name":"Belize","postal":"BZ","pop_est":307899,"iso_a2":"BZ","iso_a3":"BLZ"},"id":84,"arcs":[[2313,2314,657]]},{"type":"Polygon","properties":{"admin":"Bolivia","name":"Bolivia","postal":"BO","pop_est":9775246,"iso_a2":"BO","iso_a3":"BOL"},"id":68,"arcs":[[2315,-2256,2316,2317,2318,964,2319,2320,2321]]},{"type":"MultiPolygon","properties":{"admin":"Brazil","name":"Brazil","postal":"BR","pop_est":198739269,"iso_a2":"BR","iso_a3":"BRA"},"id":76,"arcs":[[[193]],[[194]],[[156]],[[247]],[[249]],[[251]],[[2322,2323,668,2324,2325,955,2326,2327,-2253,2328,2329,2330,-2322,2331,2332,2333,2334]]]},{"type":"MultiPolygon","properties":{"admin":"Brunei","name":"Brunei","postal":"BN","pop_est":388190,"iso_a2":"BN","iso_a3":"BRN"},"id":96,"arcs":[[[265,2335]],[[2336,2337,264]]]},{"type":"Polygon","properties":{"admin":"Bhutan","name":"Bhutan","postal":"BT","pop_est":691141,"iso_a2":"BT","iso_a3":"BTN"},"id":64,"arcs":[[2338,2339]]},{"type":"Polygon","properties":{"admin":"Botswana","name":"Botswana","postal":"BW","pop_est":1990876,"iso_a2":"BW","iso_a3":"BWA"},"id":72,"arcs":[[2340,2341,2342]]},{"type":"Polygon","properties":{"admin":"Central African Republic","name":"Central African Rep.","postal":"CF","pop_est":4511488,"iso_a2":"CF","iso_a3":"CAF"},"id":140,"arcs":[[2343,2344,2345,2346,2347,2348]]},{"type":"MultiPolygon","properties":{"admin":"Canada","name":"Canada","postal":"CA","pop_est":33487208,"iso_a2":"CA","iso_a3":"CAN"},"id":124,"arcs":[[[1500,1501]],[[2216,2217,2218,2215]],[[1490,1491,1492,1493,1494,1495,1496,1497]],[[1480,1481,1482,1483,1484,1485,1486,1487,1488,1489]],[[500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,2349,2350,2351,986,2352,2353,2354,2355,2356,2357,2358,2359]],[[1473,1474,1475,1476,1477,1478]],[[2142,2143,2144,2145,2146,2147,2148,2149,2150,2151,2152,2153,2154,2155,2156]],[[2157,2158,2159,2160,2161,2162,2163,2164,2165,2166,2167,2168,2169,2170,2171,2172,2173,2174,2175,2176,2177,2178,2179,2180,2181,2182,2183,2184,2185,2186,2187,2188,2189,2190,2191,2192,2193,2194,2195,2196]],[[2201,2202,2203,2204]],[[1513,1514,1515,1516]],[[1517,1518,1519,1520]],[[1524,1525,1526]],[[1527,1528,1529,1530,1531,1532,1533]],[[1563,1564,1565]],[[1994,1995]],[[1800,1801]],[[1854,1855,1856,1857]],[[1820,1821,1822,1823]],[[1858,1859,1860,1861,2360,1863,1864,1865]],[[1831,1832,1833]],[[1834]],[[1835,1836,1837,1838,1839,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851]],[[1868,1869,1870]],[[1871,1872]],[[1806,1807]],[[1815,1816,1817]],[[1802,1803,1804,1805]],[[1818,1819]],[[1811,1812,1813,1814]],[[2012,2013]],[[2015,2016]],[[2005,2006,2007,2008,2009,2010,2011]],[[2361,2362,2363,1000,1001,1002,1003,1004,1005,1006,1007,2364,2365,1027,1028,1029,1019,1020,2366,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,2367,2368,2369,2370,2371,2372,2373,2374,2375,2376,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,2377,2378,2379,2380,2381,2382,2383,2384,2385,2386,2387,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499]],[[2027,2028,2029,2030,2031,2032,2033,2034,2035,2036,2037,2038,2039,2040,2041,2042,2043,2044,2045,2046,2047,2048,2049,2050,2051,2052,2053,2054,2055,2056,2057,2058,2059,2060,2061,2062,2063,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073]],[[2115,2116,2117,2118]],[[2131,2132,2133,2134,2135,2136,2137,2138]],[[1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,1886,1887,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990]],[[2091,2092,2093,2094,2095,2096,2097,2098,2099,2100,2101,2102,2103,2104,2105,2106,2107,2108,2109,2110,2111,2112]],[[2026]],[[2120,2121,2122,2123,2124,2125,2126,2127,2128,2129,2130]],[[2074,2075,2076,2077,2078,2079,2080,2081,2082,2083,2084,2085,2086,2087,2088,2089,2090]],[[1613,1614,1615,1616]],[[1617,1618,1619,1620,1621]],[[1689]],[[1703,1704,1705]],[[1706,1707,1708]],[[1690,1691,1692,1693,1694,1695,1696,1697,1698,1699,1700,1701,1702]],[[1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664]],[[1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687,1688]],[[1710,1711,1712,1713,1714,1715,1716,1717,1718,1719,1720]],[[1626,1627,1628]],[[1633]],[[1634,1635,1636]],[[1623,1624,1625]],[[1630,1631,1632]],[[267]],[[268,269,270,271]],[[274,275,276,277,278,279,280,281,282,283]],[[308,309]],[[284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306]],[[1722,1723,1724,1725,1726,1727,1728,1729,1730,1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748,1749,1750,1751,1752,1753,1754,1755,1756,1757,1758,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791]]]},{"type":"Polygon","properties":{"admin":"Switzerland","name":"Switzerland","postal":"CH","pop_est":7604467,"iso_a2":"CH","iso_a3":"CHE"},"id":756,"arcs":[[1362,-2268,2388,-2266,2389,2390,2391,2392,2393]]},{"type":"MultiPolygon","properties":{"admin":"Chile","name":"Chile","postal":"CL","pop_est":16601707,"iso_a2":"CL","iso_a3":"CHL"},"id":152,"arcs":[[[30]],[[29]],[[28]],[[27]],[[114]],[[113]],[[-2251,116]],[[149]],[[121]],[[117]],[[122]],[[120]],[[118]],[[123]],[[125]],[[124]],[[131]],[[-2255,671,2394,-2317]]]},{"type":"MultiPolygon","properties":{"admin":"China","name":"China","postal":"CN","pop_est":1338612970,"iso_a2":"CN","iso_a3":"CHN"},"id":156,"arcs":[[[101]],[[1441]],[[2395,1352,2396,2397,2398,1152,2399,1155,2400,1157,2401,2402,2403,2404,-2340,2405,2406,2407,2408,2409,2410,2411,2412,2413,2414,2415,2416,2417,2418,2419,-2229,2420,2421,2422,2423,2424,2425]]]},{"type":"Polygon","properties":{"admin":"Ivory Coast","name":"Côte d'Ivoire","postal":"CI","pop_est":20617068,"iso_a2":"CI","iso_a3":"CIV"},"id":384,"arcs":[[-2296,2426,1206,2427,2428,2429]]},{"type":"Polygon","properties":{"admin":"Cameroon","name":"Cameroon","postal":"CM","pop_est":18879301,"iso_a2":"CM","iso_a3":"CMR"},"id":120,"arcs":[[-2348,2430,2431,2432,1199,2433,2434,2435]]},{"type":"Polygon","properties":{"admin":"Democratic Republic of the Congo","name":"Dem. Rep. Congo","postal":"DRC","pop_est":68692542,"iso_a2":"CD","iso_a3":"COD"},"id":180,"arcs":[[2436,2437,2438,1316,2439,2440,1304,2441,2442,2443,2444,1290,2445,2446,-2280,2447,1311,2448,2449,-2238,1194,-2240,2450,-2346]]},{"type":"Polygon","properties":{"admin":"Republic of Congo","name":"Congo","postal":"CG","pop_est":4012809,"iso_a2":"CG","iso_a3":"COG"},"id":178,"arcs":[[-2451,-2239,1196,2451,-2431,-2347]]},{"type":"Polygon","properties":{"admin":"Colombia","name":"Colombia","postal":"CO","pop_est":45644023,"iso_a2":"CO","iso_a3":"COL"},"id":170,"arcs":[[2452,-2333,2453,2454,674,2455,663]]},{"type":"MultiPolygon","properties":{"admin":"Comoros","name":"Comoros","postal":"KM","pop_est":752438,"iso_a2":"KM","iso_a3":"COM"},"id":174,"arcs":[[[238]],[[240]]]},{"type":"MultiPolygon","properties":{"admin":"Cape Verde","name":"Cape Verde","postal":"CV","pop_est":429474,"iso_a2":"CV","iso_a3":"CPV"},"id":132,"arcs":[[[81]],[[83]],[[86]],[[85]]]},{"type":"Polygon","properties":{"admin":"Costa Rica","name":"Costa Rica","postal":"CR","pop_est":4253877,"iso_a2":"CR","iso_a3":"CRI"},"id":188,"arcs":[[661,2456,676,2457]]},{"type":"MultiPolygon","properties":{"admin":"Cuba","name":"Cuba","postal":"CU","pop_est":11451652,"iso_a2":"CU","iso_a3":"CUB"},"id":192,"arcs":[[[325]],[[326]]]},{"type":"Polygon","properties":{"admin":"Northern Cyprus","name":"N. Cyprus","postal":"CN","pop_est":265100,"iso_a2":"-99","iso_a3":"-99"},"id":-99,"arcs":[[2458,1449]]},{"type":"Polygon","properties":{"admin":"Cyprus","name":"Cyprus","postal":"CY","pop_est":531640,"iso_a2":"CY","iso_a3":"CYP"},"id":196,"arcs":[[-2459,1448]]},{"type":"Polygon","properties":{"admin":"Czech Republic","name":"Czech Rep.","postal":"CZ","pop_est":10211904,"iso_a2":"CZ","iso_a3":"CZE"},"id":203,"arcs":[[2459,2460,-2271,2461]]},{"type":"MultiPolygon","properties":{"admin":"Germany","name":"Germany","postal":"D","pop_est":82329758,"iso_a2":"DE","iso_a3":"DEU"},"id":276,"arcs":[[[2206]],[[2462,1256,2463,2464,-2462,-2270,2465,1361,-2394,-2393,-2392,2466,2467,-2284,2468,1252,2469,2470]]]},{"type":"Polygon","properties":{"admin":"Djibouti","name":"Djibouti","postal":"DJ","pop_est":516055,"iso_a2":"DJ","iso_a3":"DJI"},"id":262,"arcs":[[2471,2472,2473,1185]]},{"type":"Polygon","properties":{"admin":"Dominica","name":"Dominica","postal":"DM","pop_est":72660,"iso_a2":"DM","iso_a3":"DMA"},"id":212,"arcs":[[80]]},{"type":"MultiPolygon","properties":{"admin":"Denmark","name":"Denmark","postal":"DK","pop_est":5500510,"iso_a2":"DK","iso_a3":"DNK"},"id":208,"arcs":[[[1534]],[[1542]],[[1546]],[[1543]],[[2474,-2471,2475,1254]]]},{"type":"Polygon","properties":{"admin":"Dominican Republic","name":"Dominican Rep.","postal":"DO","pop_est":9650054,"iso_a2":"DO","iso_a3":"DOM"},"id":214,"arcs":[[2476,98]]},{"type":"Polygon","properties":{"admin":"Algeria","name":"Algeria","postal":"DZ","pop_est":34178188,"iso_a2":"DZ","iso_a3":"DZA"},"id":12,"arcs":[[2477,2478,2479,2480,2481,2482,2483,1220]]},{"type":"MultiPolygon","properties":{"admin":"Ecuador","name":"Ecuador","postal":"EC","pop_est":14573101,"iso_a2":"EC","iso_a3":"ECU"},"id":218,"arcs":[[[173]],[[195]],[[188]],[[2484,673,-2455]]]},{"type":"Polygon","properties":{"admin":"Egypt","name":"Egypt","postal":"EG","pop_est":83082869,"iso_a2":"EG","iso_a3":"EGY"},"id":818,"arcs":[[2485,2486,1182,2487,2488,1223]]},{"type":"MultiPolygon","properties":{"admin":"Eritrea","name":"Eritrea","postal":"ER","pop_est":5647168,"iso_a2":"ER","iso_a3":"ERI"},"id":232,"arcs":[[[1184,-2474,2489,2490]]]},{"type":"MultiPolygon","properties":{"admin":"Spain","name":"Spain","postal":"E","pop_est":40525002,"iso_a2":"ES","iso_a3":"ESP"},"id":724,"arcs":[[[1443]],[[1445]],[[1446]],[[1438]],[[1460]],[[1465]],[[2491,-2493,2493,1245,2494,1247]]]},{"type":"MultiPolygon","properties":{"admin":"Estonia","name":"Estonia","postal":"EST","pop_est":1299371,"iso_a2":"EE","iso_a3":"EST"},"id":233,"arcs":[[[1609]],[[1554]],[[2495,2496,1397,2497,2498,2499,1268]]]},{"type":"Polygon","properties":{"admin":"Ethiopia","name":"Ethiopia","postal":"ET","pop_est":85237338,"iso_a2":"ET","iso_a3":"ETH"},"id":231,"arcs":[[-2473,2500,2501,2502,2503,2504,-2490]]},{"type":"Polygon","properties":{"admin":"Finland","name":"Finland","postal":"FIN","pop_est":5250275,"iso_a2":"FI","iso_a3":"FIN"},"id":246,"arcs":[[2505,1270,2506,2507]]},{"type":"MultiPolygon","properties":{"admin":"Fiji","name":"Fiji","postal":"FJ","pop_est":944720,"iso_a2":"FJ","iso_a3":"FJI"},"id":242,"arcs":[[[142]],[[146]]]},{"type":"MultiPolygon","properties":{"admin":"Falkland Islands","name":"Falkland Is.","postal":"FK","pop_est":3140,"iso_a2":"FK","iso_a3":"FLK"},"id":238,"arcs":[[[151]],[[150]]]},{"type":"MultiPolygon","properties":{"admin":"France","name":"France","postal":"F","pop_est":64057792,"iso_a2":"FR","iso_a3":"FRA"},"id":250,"arcs":[[[139]],[[-2324,2508,667]],[[87]],[[82]],[[84]],[[1459]],[[2509,-2467,-2391,2510,2511,1244,-2494,-2513,-2492,1248,-2286]]]},{"type":"Polygon","properties":{"admin":"Faroe Islands","name":"Faeroe Is.","postal":"FO","pop_est":48856,"iso_a2":"FO","iso_a3":"FRO"},"id":234,"arcs":[[1853]]},{"type":"Polygon","properties":{"admin":"Gabon","name":"Gabon","postal":"GA","pop_est":1514993,"iso_a2":"GA","iso_a3":"GAB"},"id":266,"arcs":[[-2452,1197,2513,-2432]]},{"type":"MultiPolygon","properties":{"admin":"United Kingdom","name":"United Kingdom","postal":"GB","pop_est":62262000,"iso_a2":"GB","iso_a3":"GBR"},"id":826,"arcs":[[[2205]],[[2514,2208]],[[1553]],[[1569]],[[1600]],[[1607]],[[2209]],[[1993]]]},{"type":"Polygon","properties":{"admin":"Georgia","name":"Georgia","postal":"GE","pop_est":4615807,"iso_a2":"GE","iso_a3":"GEO"},"id":268,"arcs":[[-2275,-2260,2515,1229,2516]]},{"type":"Polygon","properties":{"admin":"Ghana","name":"Ghana","postal":"GH","pop_est":23832495,"iso_a2":"GH","iso_a3":"GHA"},"id":288,"arcs":[[2517,1203,2518,1205,-2427,-2295]]},{"type":"Polygon","properties":{"admin":"Guinea","name":"Guinea","postal":"GN","pop_est":10057975,"iso_a2":"GN","iso_a3":"GIN"},"id":324,"arcs":[[2519,-2429,2520,2521,1209,2522,2523]]},{"type":"Polygon","properties":{"admin":"Gambia","name":"Gambia","postal":"GM","pop_est":1782893,"iso_a2":"GM","iso_a3":"GMB"},"id":270,"arcs":[[1212,2524]]},{"type":"Polygon","properties":{"admin":"Guinea Bissau","name":"Guinea-Bissau","postal":"GW","pop_est":1533964,"iso_a2":"GW","iso_a3":"GNB"},"id":624,"arcs":[[1210,2525,-2523]]},{"type":"MultiPolygon","properties":{"admin":"Equatorial Guinea","name":"Eq. Guinea","postal":"GQ","pop_est":650702,"iso_a2":"GQ","iso_a3":"GNQ"},"id":226,"arcs":[[[1198,-2433,-2514]],[[106]]]},{"type":"MultiPolygon","properties":{"admin":"Greece","name":"Greece","postal":"GR","pop_est":10737428,"iso_a2":"GR","iso_a3":"GRC"},"id":300,"arcs":[[[1451]],[[1447]],[[1454]],[[1455]],[[1462]],[[1463]],[[1461]],[[1464]],[[1235,-2243,2526,-2301,2527]]]},{"type":"MultiPolygon","properties":{"admin":"Greenland","name":"Greenland","postal":"GL","pop_est":57600,"iso_a2":"GL","iso_a3":"GRL"},"id":304,"arcs":[[[2017]],[[2018]],[[2003]],[[2025]],[[1622]],[[1709]],[[307]],[[312]],[[2113]]]},{"type":"Polygon","properties":{"admin":"Guatemala","name":"Guatemala","postal":"GT","pop_est":13276517,"iso_a2":"GT","iso_a3":"GTM"},"id":320,"arcs":[[-2314,658,2528,2529,680,2530]]},{"type":"Polygon","properties":{"admin":"Guam","name":"Guam","postal":"GU","pop_est":178430,"iso_a2":"GU","iso_a3":"GUM"},"id":316,"arcs":[[88]]},{"type":"Polygon","properties":{"admin":"Guyana","name":"Guyana","postal":"GY","pop_est":772298,"iso_a2":"GY","iso_a3":"GUY"},"id":328,"arcs":[[2531,-2335,2532,665]]},{"type":"Polygon","properties":{"admin":"Hong Kong S.A.R.","name":"Hong Kong","postal":"HK","pop_est":7061200,"iso_a2":"HK","iso_a3":"HKG"},"id":344,"arcs":[[-2400,2533,1154]]},{"type":"Polygon","properties":{"admin":"Honduras","name":"Honduras","postal":"HN","pop_est":7792854,"iso_a2":"HN","iso_a3":"HND"},"id":340,"arcs":[[2534,678,2535,-2529,659]]},{"type":"MultiPolygon","properties":{"admin":"Croatia","name":"Croatia","postal":"HR","pop_est":4489409,"iso_a2":"HR","iso_a3":"HRV"},"id":191,"arcs":[[[-2307,2536,1238]],[[2537,-2308,1240,2538,2539]]]},{"type":"MultiPolygon","properties":{"admin":"Haiti","name":"Haiti","postal":"HT","pop_est":9035536,"iso_a2":"HT","iso_a3":"HTI"},"id":332,"arcs":[[[-2477,99]]]},{"type":"Polygon","properties":{"admin":"Hungary","name":"Hungary","postal":"HU","pop_est":9905596,"iso_a2":"HU","iso_a3":"HUN"},"id":348,"arcs":[[2540,2541,2542,-2540,2543,-2263,2544]]},{"type":"MultiPolygon","properties":{"admin":"Indonesia","name":"Indonesia","postal":"INDO","pop_est":240271522,"iso_a2":"ID","iso_a3":"IDN"},"id":360,"arcs":[[[245]],[[203]],[[2545,207,2546,2547,2548,210,2549,2550]],[[214]],[[219]],[[218]],[[223]],[[220]],[[216]],[[213]],[[217]],[[225]],[[226]],[[228]],[[153]],[[155]],[[227]],[[164]],[[167]],[[169]],[[157]],[[171]],[[158]],[[162]],[[159]],[[160]],[[175]],[[176]],[[177]],[[178]],[[179]],[[182]],[[181]],[[183]],[[180]],[[185]],[[184]],[[187]],[[190]],[[189]],[[192]],[[191]],[[2551,2552,2553,2554,232]],[[197]],[[196]],[[198]],[[248]],[[253]],[[254]],[[255]],[[256]],[[252]],[[257]],[[186]],[[102]],[[259]],[[103]],[[104]],[[107]],[[2555,261,2556]],[[105]],[[258]]]},{"type":"Polygon","properties":{"admin":"Isle of Man","name":"Isle of Man","postal":"IM","pop_est":76512,"iso_a2":"IM","iso_a3":"IMN"},"id":833,"arcs":[[2200]]},{"type":"MultiPolygon","properties":{"admin":"India","name":"India","postal":"IND","pop_est":1166079220,"iso_a2":"IN","iso_a3":"IND"},"id":356,"arcs":[[[111]],[[52]],[[92]],[[-2412,2557,-2410,2558,-2408,2559,-2406,-2339,-2405,2560,-2299,1165,2561,2562,2563,-2417,2564,-2415,2565,-2413]]]},{"type":"Polygon","properties":{"admin":"Ireland","name":"Ireland","postal":"IRL","pop_est":4203200,"iso_a2":"IE","iso_a3":"IRL"},"id":372,"arcs":[[2207,-2515]]},{"type":"MultiPolygon","properties":{"admin":"Iran","name":"Iran","postal":"IRN","pop_est":66429284,"iso_a2":"IR","iso_a3":"IRN"},"id":364,"arcs":[[[1435]],[[-2257,-2274,1275,2566,-2232,2567,2568,2569,1168,2570,2571,-2272]]]},{"type":"Polygon","properties":{"admin":"Iraq","name":"Iraq","postal":"IRQ","pop_est":31129225,"iso_a2":"IQ","iso_a3":"IRQ"},"id":368,"arcs":[[-2571,1169,2572,2573,2574,2575,2576]]},{"type":"Polygon","properties":{"admin":"Iceland","name":"Iceland","postal":"IS","pop_est":306694,"iso_a2":"IS","iso_a3":"ISL"},"id":352,"arcs":[[1852]]},{"type":"Polygon","properties":{"admin":"Israel","name":"Israel","postal":"IS","pop_est":7233701,"iso_a2":"IL","iso_a3":"ISR"},"id":376,"arcs":[[2577,2578,2579,2580,2581,1181,-2487,2582,1225,2583,2584]]},{"type":"MultiPolygon","properties":{"admin":"Italy","name":"Italy","postal":"I","pop_est":58126212,"iso_a2":"IT","iso_a3":"ITA"},"id":380,"arcs":[[[1452]],[[1466]],[[2585,1242,-2511,-2390,-2265]]]},{"type":"Polygon","properties":{"admin":"Jamaica","name":"Jamaica","postal":"J","pop_est":2825928,"iso_a2":"JM","iso_a3":"JAM"},"id":388,"arcs":[[97]]},{"type":"Polygon","properties":{"admin":"Jordan","name":"Jordan","postal":"J","pop_est":6342948,"iso_a2":"JO","iso_a3":"JOR"},"id":400,"arcs":[[2586,1180,-2582,-2581,-2580,2587,2588,-2578,2589,-2575]]},{"type":"MultiPolygon","properties":{"admin":"Japan","name":"Japan","postal":"J","pop_est":127078679,"iso_a2":"JP","iso_a3":"JPN"},"id":392,"arcs":[[[1432]],[[1444]],[[1467]],[[1470]],[[1450]],[[1453]],[[1471]],[[1499]]]},{"type":"Polygon","properties":{"admin":"Siachen Glacier","name":"Siachen Glacier","postal":"SG","pop_est":6000,"iso_a2":"-99","iso_a3":"-99"},"id":-99,"arcs":[[-2563,2590,-2419]]},{"type":"MultiPolygon","properties":{"admin":"Kazakhstan","name":"Kazakhstan","postal":"KZ","pop_est":15399437,"iso_a2":"KZ","iso_a3":"KAZ"},"id":398,"arcs":[[[2591,2592,2593,1372]],[[-2423,2594,2595,2596,1376,2597,2598,2599,1277,2600]]]},{"type":"Polygon","properties":{"admin":"Kenya","name":"Kenya","postal":"KE","pop_est":39002772,"iso_a2":"KE","iso_a3":"KEN"},"id":404,"arcs":[[2601,1188,2602,2603,1299,2604,2605,2606,-2503]]},{"type":"Polygon","properties":{"admin":"Kyrgyzstan","name":"Kyrgyzstan","postal":"KG","pop_est":5431747,"iso_a2":"KG","iso_a3":"KGZ"},"id":417,"arcs":[[-2422,2607,2608,-2595]]},{"type":"Polygon","properties":{"admin":"Cambodia","name":"Cambodia","postal":"KH","pop_est":14494293,"iso_a2":"KH","iso_a3":"KHM"},"id":116,"arcs":[[1159,2609,2610,2611]]},{"type":"MultiPolygon","properties":{"admin":"South Korea","name":"Korea","postal":"KR","pop_est":48508972,"iso_a2":"KR","iso_a3":"KOR"},"id":410,"arcs":[[[1469]],[[1150,2612]]]},{"type":"Polygon","properties":{"admin":"Kosovo","name":"Kosovo","postal":"KO","pop_est":1804838,"iso_a2":"-99","iso_a3":"-99"},"id":-99,"arcs":[[2613,-2241,2614,2615]]},{"type":"MultiPolygon","properties":{"admin":"Kuwait","name":"Kuwait","postal":"KW","pop_est":2691158,"iso_a2":"KW","iso_a3":"KWT"},"id":414,"arcs":[[[1442]],[[2616,-2573,1170]]]},{"type":"Polygon","properties":{"admin":"Laos","name":"Lao PDR","postal":"LA","pop_est":6834942,"iso_a2":"LA","iso_a3":"LAO"},"id":418,"arcs":[[2617,-2611,2618,2619,-2403]]},{"type":"Polygon","properties":{"admin":"Lebanon","name":"Lebanon","postal":"LB","pop_est":4017095,"iso_a2":"LB","iso_a3":"LBN"},"id":422,"arcs":[[-2584,1226,2620]]},{"type":"Polygon","properties":{"admin":"Liberia","name":"Liberia","postal":"LR","pop_est":3441790,"iso_a2":"LR","iso_a3":"LBR"},"id":430,"arcs":[[-2428,1207,2621,-2521]]},{"type":"Polygon","properties":{"admin":"Libya","name":"Libya","postal":"LY","pop_est":6310434,"iso_a2":"LY","iso_a3":"LBY"},"id":434,"arcs":[[-2489,2622,2623,2624,-2479,2625,1222]]},{"type":"Polygon","properties":{"admin":"Sri Lanka","name":"Sri Lanka","postal":"LK","pop_est":21324791,"iso_a2":"LK","iso_a3":"LKA"},"id":144,"arcs":[[108]]},{"type":"Polygon","properties":{"admin":"Lesotho","name":"Lesotho","postal":"LS","pop_est":2130819,"iso_a2":"LS","iso_a3":"LSO"},"id":426,"arcs":[[2626]]},{"type":"Polygon","properties":{"admin":"Lithuania","name":"Lithuania","postal":"LT","pop_est":3555179,"iso_a2":"LT","iso_a3":"LTU"},"id":440,"arcs":[[-2312,2627,2628,2629,1266,2630]]},{"type":"Polygon","properties":{"admin":"Luxembourg","name":"Luxembourg","postal":"L","pop_est":491775,"iso_a2":"LU","iso_a3":"LUX"},"id":442,"arcs":[[-2468,-2510,-2285]]},{"type":"Polygon","properties":{"admin":"Latvia","name":"Latvia","postal":"LV","pop_est":2231503,"iso_a2":"LV","iso_a3":"LVA"},"id":428,"arcs":[[2631,-2313,-2631,1267,-2500]]},{"type":"Polygon","properties":{"admin":"Morocco","name":"Morocco","postal":"MA","pop_est":34859364,"iso_a2":"MA","iso_a3":"MAR"},"id":504,"arcs":[[-2484,2632,2633,1217,2634,1219]]},{"type":"Polygon","properties":{"admin":"Moldova","name":"Moldova","postal":"MD","pop_est":4320748,"iso_a2":"MD","iso_a3":"MDA"},"id":498,"arcs":[[2635,2636]]},{"type":"Polygon","properties":{"admin":"Madagascar","name":"Madagascar","postal":"MG","pop_est":20653556,"iso_a2":"MG","iso_a3":"MDG"},"id":450,"arcs":[[235]]},{"type":"MultiPolygon","properties":{"admin":"Mexico","name":"Mexico","postal":"MX","pop_est":111211789,"iso_a2":"MX","iso_a3":"MEX"},"id":484,"arcs":[[[1439]],[[1440]],[[2637,2638,2639,2640,2641,2642,2643,2644,2645,2646,2647,2648,2649,2650,2651,2652,2653,2654,656,-2315,-2531,681,2655,2656,2657]]]},{"type":"Polygon","properties":{"admin":"Macedonia","name":"Macedonia","postal":"MK","pop_est":2066718,"iso_a2":"MK","iso_a3":"MKD"},"id":807,"arcs":[[-2302,-2527,-2242,-2614,2658]]},{"type":"Polygon","properties":{"admin":"Mali","name":"Mali","postal":"ML","pop_est":12666987,"iso_a2":"ML","iso_a3":"MLI"},"id":466,"arcs":[[2659,-2297,-2430,-2520,2660,2661,-2481]]},{"type":"MultiPolygon","properties":{"admin":"Myanmar","name":"Myanmar","postal":"MM","pop_est":48137741,"iso_a2":"MM","iso_a3":"MMR"},"id":104,"arcs":[[[-2620,2662,1163,-2298,-2561,-2404]]]},{"type":"Polygon","properties":{"admin":"Montenegro","name":"Montenegro","postal":"ME","pop_est":672180,"iso_a2":"ME","iso_a3":"MNE"},"id":499,"arcs":[[2663,-2615,-2247,2664,1383,2665,-2244,1237,-2537,-2306]]},{"type":"Polygon","properties":{"admin":"Mongolia","name":"Mongolia","postal":"MN","pop_est":3041142,"iso_a2":"MN","iso_a3":"MNG"},"id":496,"arcs":[[-2425,2666]]},{"type":"Polygon","properties":{"admin":"Mozambique","name":"Mozambique","postal":"MZ","pop_est":21669278,"iso_a2":"MZ","iso_a3":"MOZ"},"id":508,"arcs":[[2667,2668,2669,2670,2671,2672,2673,1284,2674,2675,1190]]},{"type":"Polygon","properties":{"admin":"Mauritania","name":"Mauritania","postal":"MR","pop_est":3129486,"iso_a2":"MR","iso_a3":"MRT"},"id":478,"arcs":[[2676,1214,2677,-2482,-2662]]},{"type":"Polygon","properties":{"admin":"Mauritius","name":"Mauritius","postal":"MU","pop_est":1284264,"iso_a2":"MU","iso_a3":"MUS"},"id":480,"arcs":[[141]]},{"type":"Polygon","properties":{"admin":"Malawi","name":"Malawi","postal":"MW","pop_est":14268711,"iso_a2":"MW","iso_a3":"MWI"},"id":454,"arcs":[[1287,2678,-2673,2679,2680]]},{"type":"MultiPolygon","properties":{"admin":"Malaysia","name":"Malaysia","postal":"MY","pop_est":25715819,"iso_a2":"MY","iso_a3":"MYS"},"id":458,"arcs":[[[1161,2681]],[[2682,-2557,262,2683,-2337,-2336,266]]]},{"type":"Polygon","properties":{"admin":"Namibia","name":"Namibia","postal":"NA","pop_est":2108665,"iso_a2":"NA","iso_a3":"NAM"},"id":516,"arcs":[[2684,-2343,2685,1192,-2237]]},{"type":"MultiPolygon","properties":{"admin":"New Caledonia","name":"New Caledonia","postal":"NC","pop_est":227436,"iso_a2":"NC","iso_a3":"NCL"},"id":540,"arcs":[[[140]],[[138]]]},{"type":"Polygon","properties":{"admin":"Niger","name":"Niger","postal":"NE","pop_est":15306252,"iso_a2":"NE","iso_a3":"NER"},"id":562,"arcs":[[2686,2687,-2292,-2293,-2660,-2480,-2625]]},{"type":"Polygon","properties":{"admin":"Nigeria","name":"Nigeria","postal":"NG","pop_est":149229090,"iso_a2":"NG","iso_a3":"NGA"},"id":566,"arcs":[[2688,-2434,1200,-2289,-2688]]},{"type":"Polygon","properties":{"admin":"Nicaragua","name":"Nicaragua","postal":"NI","pop_est":5891199,"iso_a2":"NI","iso_a3":"NIC"},"id":558,"arcs":[[660,-2458,677,-2535]]},{"type":"MultiPolygon","properties":{"admin":"Netherlands","name":"Netherlands","postal":"NL","pop_est":16715999,"iso_a2":"NL","iso_a3":"NLD"},"id":528,"arcs":[[[-2287,1250]],[[2224]],[[1251,-2469,-2283,2689,-2288],[1388]]]},{"type":"MultiPolygon","properties":{"admin":"Norway","name":"Norway","postal":"N","pop_est":4676305,"iso_a2":"NO","iso_a3":"NOR"},"id":578,"arcs":[[[1810]],[[1809]],[[2014]],[[2019]],[[2002]],[[2690,-2508,2691,1143]],[[2021]],[[1629]],[[1721]],[[313]]]},{"type":"Polygon","properties":{"admin":"Nepal","name":"Nepal","postal":"NP","pop_est":28563377,"iso_a2":"NP","iso_a3":"NPL"},"id":524,"arcs":[[-2560,-2407]]},{"type":"MultiPolygon","properties":{"admin":"New Zealand","name":"New Zealand","postal":"NZ","pop_est":4213418,"iso_a2":"NZ","iso_a3":"NZL"},"id":554,"arcs":[[[126]],[[45]],[[130]],[[133]]]},{"type":"MultiPolygon","properties":{"admin":"Oman","name":"Oman","postal":"OM","pop_est":3418085,"iso_a2":"OM","iso_a3":"OMN"},"id":512,"arcs":[[[78]],[[1177,2692,2693,-2248]],[[-2250,1175]]]},{"type":"Polygon","properties":{"admin":"Pakistan","name":"Pakistan","postal":"PK","pop_est":176242949,"iso_a2":"PK","iso_a3":"PAK"},"id":586,"arcs":[[-2591,-2562,1166,2694,-2569,-2230,-2420]]},{"type":"MultiPolygon","properties":{"admin":"Panama","name":"Panama","postal":"PA","pop_est":3360474,"iso_a2":"PA","iso_a3":"PAN"},"id":591,"arcs":[[[56]],[[-2456,675,-2457,662]]]},{"type":"Polygon","properties":{"admin":"Peru","name":"Peru","postal":"PE","pop_est":29546963,"iso_a2":"PE","iso_a3":"PER"},"id":604,"arcs":[[-2332,-2321,2695,966,2696,962,2697,-2318,-2395,672,-2485,-2454]]},{"type":"MultiPolygon","properties":{"admin":"Philippines","name":"Philippines","postal":"PH","pop_est":97976603,"iso_a2":"PH","iso_a3":"PHL"},"id":608,"arcs":[[[110]],[[109]],[[112]],[[59]],[[49]],[[60]],[[61]],[[58]],[[53]],[[54]],[[79]],[[55]],[[89]],[[90]],[[93]],[[94]],[[95]],[[96]]]},{"type":"MultiPolygon","properties":{"admin":"Papua New Guinea","name":"Papua New Guinea","postal":"PG","pop_est":6057263,"iso_a2":"PG","iso_a3":"PNG"},"id":598,"arcs":[[[244]],[[202]],[[200]],[[201]],[[204]],[[166]],[[163]],[[165]],[[170]],[[2698,-2554,2699,2700,230]],[[161]],[[172]],[[174]]]},{"type":"Polygon","properties":{"admin":"Poland","name":"Poland","postal":"PL","pop_est":38482919,"iso_a2":"PL","iso_a3":"POL"},"id":616,"arcs":[[2701,-2628,-2311,2702,2703,-2460,-2465,2704,1258,2705,-2707,1261]]},{"type":"Polygon","properties":{"admin":"Puerto Rico","name":"Puerto Rico","postal":"PR","pop_est":3971020,"iso_a2":"PR","iso_a3":"PRI"},"id":630,"arcs":[[100]]},{"type":"Polygon","properties":{"admin":"North Korea","name":"Dem. Rep. Korea","postal":"KP","pop_est":22665345,"iso_a2":"KP","iso_a3":"PRK"},"id":408,"arcs":[[2707,1149,-2613,1151,-2399]]},{"type":"MultiPolygon","properties":{"admin":"Portugal","name":"Portugal","postal":"P","pop_est":10707924,"iso_a2":"PT","iso_a3":"PRT"},"id":620,"arcs":[[[1468]],[[1246,-2495]]]},{"type":"Polygon","properties":{"admin":"Paraguay","name":"Paraguay","postal":"PY","pop_est":6995655,"iso_a2":"PY","iso_a3":"PRY"},"id":600,"arcs":[[-2331,-2330,-2329,-2252,-2316]]},{"type":"MultiPolygon","properties":{"admin":"Palestine","name":"Palestine","postal":"PAL","pop_est":4119083,"iso_a2":"PS","iso_a3":"PSE"},"id":275,"arcs":[[[-2588,-2579,-2589]]]},{"type":"MultiPolygon","properties":{"admin":"French Polynesia","name":"Fr. Polynesia","postal":"PF","pop_est":287032,"iso_a2":"PF","iso_a3":"PYF"},"id":258,"arcs":[[[143]]]},{"type":"Polygon","properties":{"admin":"Qatar","name":"Qatar","postal":"QA","pop_est":833285,"iso_a2":"QA","iso_a3":"QAT"},"id":634,"arcs":[[2708,1172]]},{"type":"Polygon","properties":{"admin":"Romania","name":"Romania","postal":"RO","pop_est":22215421,"iso_a2":"RO","iso_a3":"ROU"},"id":642,"arcs":[[2709,1232,-2304,2710,-2542,2711,-2636]]},{"type":"MultiPolygon","properties":{"admin":"Russia","name":"Russia","postal":"RUS","pop_est":140041247,"iso_a2":"RU","iso_a3":"RUS"},"id":643,"arcs":[[[1498]],[[1502]],[[1479]],[[2141]],[[2198]],[[1535]],[[2712,-2629,-2702,1262,2713,1264]],[[1541]],[[1608]],[[2714,1867]],[[1808]],[[2004]],[[2001]],[[2020]],[[2022]],[[1996]],[[2715,1998,2716,2000]],[[2024]],[[2023]],[[2114]],[[2139]],[[2119]],[[1610]],[[1611]],[[1612]],[[1666]],[[1665]],[[2140]],[[2717,1148,-2708,-2398,2718,1354,2719,-2426,-2667,-2424,-2601,1273,-2276,-2517,1230,2720,-2309,-2632,-2499,2721,1399,2722,-2496,1269,-2506,-2691,1144,2723,1146]],[[272]],[[310]],[[273]],[[311]],[[316]],[[317]],[[318]],[[314]],[[319]],[[321]],[[320]],[[315]],[[322]]]},{"type":"Polygon","properties":{"admin":"Rwanda","name":"Rwanda","postal":"RW","pop_est":10473282,"iso_a2":"RW","iso_a3":"RWA"},"id":646,"arcs":[[2724,-2281,-2447,2725,1292,2726,-2444,2727]]},{"type":"Polygon","properties":{"admin":"Western Sahara","name":"W. Sahara","postal":"WS","pop_est":-99,"iso_a2":"EH","iso_a3":"ESH"},"id":732,"arcs":[[-2483,-2678,1215,2728,-2633]]},{"type":"MultiPolygon","properties":{"admin":"Saudi Arabia","name":"Saudi Arabia","postal":"SA","pop_est":28686633,"iso_a2":"SA","iso_a3":"SAU"},"id":682,"arcs":[[[-2617,1171,-2709,1173,-2249,-2694,2729,1179,-2587,-2574]]]},{"type":"Polygon","properties":{"admin":"Sudan","name":"Sudan","postal":"SD","pop_est":25946220,"iso_a2":"SD","iso_a3":"SDN"},"id":729,"arcs":[[1183,-2491,-2505,2730,-2344,2731,-2623,-2488]]},{"type":"Polygon","properties":{"admin":"South Sudan","name":"S. Sudan","postal":"SS","pop_est":10625176,"iso_a2":"SS","iso_a3":"SSD"},"id":728,"arcs":[[-2504,-2607,2732,-2437,-2345,-2731]]},{"type":"Polygon","properties":{"admin":"Senegal","name":"Senegal","postal":"SN","pop_est":13711597,"iso_a2":"SN","iso_a3":"SEN"},"id":686,"arcs":[[-2661,-2524,-2526,1211,-2525,1213,-2677]]},{"type":"Polygon","properties":{"admin":"South Georgia and South Sandwich Islands","name":"S. Geo. and S. Sandw. Is.","postal":"GS","pop_est":30,"iso_a2":"GS","iso_a3":"SGS"},"id":239,"arcs":[[26]]},{"type":"MultiPolygon","properties":{"admin":"Solomon Islands","name":"Solomon Is.","postal":"SB","pop_est":595613,"iso_a2":"SB","iso_a3":"SLB"},"id":90,"arcs":[[[241]],[[246]],[[199]],[[215]],[[205]],[[221]],[[224]],[[222]],[[152]]]},{"type":"MultiPolygon","properties":{"admin":"Sierra Leone","name":"Sierra Leone","postal":"SL","pop_est":6440053,"iso_a2":"SL","iso_a3":"SLE"},"id":694,"arcs":[[[57]],[[-2622,1208,-2522]]]},{"type":"Polygon","properties":{"admin":"El Salvador","name":"El Salvador","postal":"SV","pop_est":7185218,"iso_a2":"SV","iso_a3":"SLV"},"id":222,"arcs":[[-2536,679,-2530]]},{"type":"Polygon","properties":{"admin":"Somaliland","name":"Somaliland","postal":"SL","pop_est":3500000,"iso_a2":"-99","iso_a3":"-99"},"id":-99,"arcs":[[2733,-2501,-2472,1186]]},{"type":"Polygon","properties":{"admin":"Somalia","name":"Somalia","postal":"SO","pop_est":9832017,"iso_a2":"SO","iso_a3":"SOM"},"id":706,"arcs":[[-2602,-2502,-2734,1187]]},{"type":"Polygon","properties":{"admin":"Republic of Serbia","name":"Serbia","postal":"RS","pop_est":7379339,"iso_a2":"RS","iso_a3":"SRB"},"id":688,"arcs":[[-2711,-2303,-2659,-2616,-2664,-2305,-2538,-2543]]},{"type":"Polygon","properties":{"admin":"Sao Tome and Principe","name":"São Tomé and Principe","postal":"ST","pop_est":212679,"iso_a2":"ST","iso_a3":"STP"},"id":678,"arcs":[[250]]},{"type":"Polygon","properties":{"admin":"Suriname","name":"Suriname","postal":"SR","pop_est":481267,"iso_a2":"SR","iso_a3":"SUR"},"id":740,"arcs":[[-2509,-2323,-2532,666]]},{"type":"Polygon","properties":{"admin":"Slovakia","name":"Slovakia","postal":"SK","pop_est":5463046,"iso_a2":"SK","iso_a3":"SVK"},"id":703,"arcs":[[2734,-2545,-2262,-2461,-2704]]},{"type":"Polygon","properties":{"admin":"Slovenia","name":"Slovenia","postal":"SLO","pop_est":2005692,"iso_a2":"SI","iso_a3":"SVN"},"id":705,"arcs":[[-2539,1241,-2586,-2264,-2544]]},{"type":"MultiPolygon","properties":{"admin":"Sweden","name":"Sweden","postal":"S","pop_est":9059651,"iso_a2":"SE","iso_a3":"SWE"},"id":752,"arcs":[[[1588]],[[1576]],[[1271,2735,1142,-2692,-2507]]]},{"type":"Polygon","properties":{"admin":"Swaziland","name":"Swaziland","postal":"SW","pop_est":1123913,"iso_a2":"SZ","iso_a3":"SWZ"},"id":748,"arcs":[[-2669,2736]]},{"type":"Polygon","properties":{"admin":"Syria","name":"Syria","postal":"SYR","pop_est":20178485,"iso_a2":"SY","iso_a3":"SYR"},"id":760,"arcs":[[-2576,-2590,-2585,-2621,1227,2737]]},{"type":"Polygon","properties":{"admin":"Chad","name":"Chad","postal":"TD","pop_est":10329208,"iso_a2":"TD","iso_a3":"TCD"},"id":148,"arcs":[[-2732,-2349,-2436,-2435,-2689,-2687,-2624]]},{"type":"Polygon","properties":{"admin":"Togo","name":"Togo","postal":"TG","pop_est":6019877,"iso_a2":"TG","iso_a3":"TGO"},"id":768,"arcs":[[-2290,1202,-2518,-2294]]},{"type":"MultiPolygon","properties":{"admin":"Thailand","name":"Thailand","postal":"TH","pop_est":65905410,"iso_a2":"TH","iso_a3":"THA"},"id":764,"arcs":[[[-2619,-2610,1160,-2682,1162,-2663]]]},{"type":"Polygon","properties":{"admin":"Tajikistan","name":"Tajikistan","postal":"TJ","pop_est":7349145,"iso_a2":"TJ","iso_a3":"TJK"},"id":762,"arcs":[[-2608,-2421,-2235,2738]]},{"type":"Polygon","properties":{"admin":"Turkmenistan","name":"Turkmenistan","postal":"TM","pop_est":4884887,"iso_a2":"TM","iso_a3":"TKM"},"id":795,"arcs":[[-2233,-2567,1276,-2600,2739,2740,1382,2741,2742]]},{"type":"MultiPolygon","properties":{"admin":"East Timor","name":"Timor-Leste","postal":"TL","pop_est":1131612,"iso_a2":"TL","iso_a3":"TLS"},"id":626,"arcs":[[[2743,2744,-2548]],[[2745,-2551,2746,212]]]},{"type":"Polygon","properties":{"admin":"Trinidad and Tobago","name":"Trinidad and Tobago","postal":"TT","pop_est":1310000,"iso_a2":"TT","iso_a3":"TTO"},"id":780,"arcs":[[48]]},{"type":"MultiPolygon","properties":{"admin":"Tunisia","name":"Tunisia","postal":"TN","pop_est":10486339,"iso_a2":"TN","iso_a3":"TUN"},"id":788,"arcs":[[[-2626,-2478,1221]]]},{"type":"MultiPolygon","properties":{"admin":"Turkey","name":"Turkey","postal":"TR","pop_est":76805524,"iso_a2":"TR","iso_a3":"TUR"},"id":792,"arcs":[[[-2516,-2259,-2273,-2572,-2577,-2738,1228]],[[1234,-2528,-2300]]]},{"type":"Polygon","properties":{"admin":"Taiwan","name":"Taiwan","postal":"TW","pop_est":22974347,"iso_a2":"TW","iso_a3":"TWN"},"id":158,"arcs":[[1436]]},{"type":"MultiPolygon","properties":{"admin":"United Republic of Tanzania","name":"Tanzania","postal":"TZ","pop_est":41048532,"iso_a2":"TZ","iso_a3":"TZA"},"id":834,"arcs":[[[154]],[[168]],[[2210]],[[2211]],[[2747,1297,2748,-2603,1189,-2676,2749,1286,-2681,2750,2751,1307,2752,-2277,-2725,2753]]]},{"type":"MultiPolygon","properties":{"admin":"Uganda","name":"Uganda","postal":"UG","pop_est":32369558,"iso_a2":"UG","iso_a3":"UGA"},"id":800,"arcs":[[[2754,1295,2755,-2754,-2728,-2443,2756,1302,2757,-2440,1317,2758,-2438,-2733,-2606]]]},{"type":"Polygon","properties":{"admin":"Ukraine","name":"Ukraine","postal":"UA","pop_est":45700395,"iso_a2":"UA","iso_a3":"UKR"},"id":804,"arcs":[[1231,-2710,-2637,-2712,-2541,-2735,-2703,-2310,-2721]]},{"type":"Polygon","properties":{"admin":"Uruguay","name":"Uruguay","postal":"UY","pop_est":3494382,"iso_a2":"UY","iso_a3":"URY"},"id":858,"arcs":[[2759,953,2760,-2325,669,-2254,-2328]]},{"type":"MultiPolygon","properties":{"admin":"United States of America","name":"United States","postal":"US","pop_est":313973000,"iso_a2":"US","iso_a3":"USA"},"id":840,"arcs":[[[73,74,75,76,77]],[[62,63]],[[64,65,66]],[[68,69,70,71,72]],[[323,324]],[[1456,1457,1458]],[[2212,2213,2214]],[[2220,2221,2222,2223,2219]],[[-2374,-2373,-2372,-2371,-2370,-2369,2761,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1030,1031,1032,1033,1034,1035,1036,1037,1038,-2367,1021,1022,1023,1024,1025,1026,-2366,2762,1009,1010,1011,1012,1013,1014,2763,-2363,-2362,-2360,-2359,-2358,-2357,-2356,-2355,-2354,2764,984,2765,-2351,-2350,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,-2655,-2654,-2653,-2652,-2651,-2650,-2649,-2648,-2647,-2646,-2645,-2644,-2643,-2642,-2641,-2640,-2639,-2638,-2658,-2657,-2656,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,-2377,-2376,-2375]],[[1506]],[[1503,1504,1505]],[[1507,1508]],[[2199]],[[1509,1510,1511,1512]],[[1521,1522,1523]],[[1536,1537,1538,1539,1540]],[[1544,1545]],[[1547,1548,1549,1550,1551,1552]],[[1555,1556,1557,1558,1559,1560,1561,1562]],[[1566,1567,1568]],[[1570,1571,1572]],[[1595,1596,1597,1598,1599]],[[1589,1590,1591,1592,1593,1594]],[[1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587]],[[1604,1605,1606]],[[1601,1602,1603]],[[1573,1574,1575]],[[1797,1798,1799]],[[1792,1793,1794,1795,1796]],[[1824,1825,1826,1827,1828,1829,1830]],[[867,868,869,870,871,872,-2388,-2387,-2386,-2385,-2384,-2383,-2382,-2381,-2380,-2379,-2378,745,746,2766,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866]]]},{"type":"MultiPolygon","properties":{"admin":"Uzbekistan","name":"Uzbekistan","postal":"UZ","pop_est":27606007,"iso_a2":"UZ","iso_a3":"UZB"},"id":860,"arcs":[[[2767,-2593,2768,1374,2769,-2596,-2609,-2739,-2234,-2743,2770,1380,2771,-2740,-2599,2772,1370]]]},{"type":"MultiPolygon","properties":{"admin":"Venezuela","name":"Venezuela","postal":"VE","pop_est":26814843,"iso_a2":"VE","iso_a3":"VEN"},"id":862,"arcs":[[[51]],[[-2533,-2334,-2453,664]]]},{"type":"MultiPolygon","properties":{"admin":"Vietnam","name":"Vietnam","postal":"VN","pop_est":86967524,"iso_a2":"VN","iso_a3":"VNM"},"id":704,"arcs":[[[50]],[[1158,-2612,-2618,-2402]]]},{"type":"MultiPolygon","properties":{"admin":"Vanuatu","name":"Vanuatu","postal":"VU","pop_est":218519,"iso_a2":"VU","iso_a3":"VUT"},"id":548,"arcs":[[[137]],[[144]],[[147]],[[145]],[[236]],[[237]]]},{"type":"MultiPolygon","properties":{"admin":"Samoa","name":"Samoa","postal":"WS","pop_est":219998,"iso_a2":"WS","iso_a3":"WSM"},"id":882,"arcs":[[[46]],[[47]]]},{"type":"MultiPolygon","properties":{"admin":"Yemen","name":"Yemen","postal":"YE","pop_est":23822783,"iso_a2":"YE","iso_a3":"YEM"},"id":887,"arcs":[[[91]],[[1178,-2730,-2693]]]},{"type":"Polygon","properties":{"admin":"South Africa","name":"South Africa","postal":"ZA","pop_est":49052489,"iso_a2":"ZA","iso_a3":"ZAF"},"id":710,"arcs":[[-2670,-2737,-2668,1191,-2686,-2342,2773],[-2627]]},{"type":"Polygon","properties":{"admin":"Zambia","name":"Zambia","postal":"ZM","pop_est":11862740,"iso_a2":"ZM","iso_a3":"ZMB"},"id":894,"arcs":[[2774,-2751,-2680,-2672,2775,2776,1279,2777,-2685,-2236,-2450,2778,1313]]},{"type":"Polygon","properties":{"admin":"Zimbabwe","name":"Zimbabwe","postal":"ZW","pop_est":12619600,"iso_a2":"ZW","iso_a3":"ZWE"},"id":716,"arcs":[[-2776,-2671,-2774,-2341,-2778,1280,2779,1282]]}]},"states":{"type":"GeometryCollection","geometries":[{"type":"Polygon","properties":{"iso_a2":"CA","name":"Alberta","postal":"AB","admin":"Canada"},"arcs":[[2780,2781,2782,2783]]},{"type":"MultiPolygon","properties":{"iso_a2":"CA","name":"British Columbia","postal":"BC","admin":"Canada"},"arcs":[[[2784,2785,2786,2787,2788,2789,2790,2791,2792,2793,2794,2795,2796,2797,2798]],[[2799,2800,2801,2802]],[[2803,2804,2805,2806]],[[2807,2808,2809]],[[2810,2811,2812,2813,2814,2815,2816]],[[2817,2818,2819,2820,2821,2822,2823,2824,2825,2826,2827,-2782,2828,2829,2830,2831,2832,2833,2834,2835,2836,2837,2838,2839,2840,2841,2842,2843,2844,2845,2846,2847,2848,2849,736,2850,2851,2852,2853,2854,2855,2856,2857]]]},{"type":"Polygon","properties":{"iso_a2":"CA","name":"Manitoba","postal":"MB","admin":"Canada"},"arcs":[[2858,2859,2860,2861,2862,2863,2864,2865,2866,2867,2868,2869,2870,2871,2872,2873,2874]]},{"type":"Polygon","properties":{"iso_a2":"CA","name":"New Brunswick","postal":"NB","admin":"Canada"},"arcs":[[2875,2876,2877,2878,2879,2880,2881,2882,2883,2884,2885,2886,2887,2888,2889,2890,2891]]},{"type":"MultiPolygon","properties":{"iso_a2":"CA","name":"Newfoundland and Labrador","postal":"NL","admin":"Canada"},"arcs":[[[2892,2893,2894,2895,2896,2897,2898,2899,2900,2901,2902,2903,2904,2905,2906,2907,2908,2909,2910,2911,2912,2913,2914,2915,2916,2917,2918,2919,2920,2921,2922,2923,2924,2925,2926,2927,2928,2929,2930,2931]],[[2932,2933,2934,2935,2936,2937,2938,2939,2940,2941,2942,2943,2944,2945,2946,2947,2948,2949,2950,2951,2952,2953,2954,2955,2956,2957,2958,2959,2960,2961,2962,2963,2964,2965,2966,2967,2968,2969,2970,2971,2972]]]},{"type":"MultiPolygon","properties":{"iso_a2":"CA","name":"Nova Scotia","postal":"NS","admin":"Canada"},"arcs":[[[-2876,2973,2974,2975,2976,2977,2978,2979,2980,2981,2982,2983,2984,2985,2986,2987,2988,2989,2990,2991]],[[2992,2993,2994,2995,2996,2997,2998,2999]]]},{"type":"MultiPolygon","properties":{"iso_a2":"CA","name":"Northwest Territories","postal":"NT","admin":"Canada"},"arcs":[[[3000,3001,-2783,-2828,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023,3024,3025,3026]],[[3027,3028,3029,3030,3031,3032,3033,3034,3035,3036,3037,3038,3039,3040,3041,3042,3043,3044,3045,3046,3047,3048,3049,3050,3051]],[[3052,3053,3054,3055,3056,3057,3058,3059,3060,3061,3062,3063,3064,3065,3066,3067,3068]],[[3069]],[[3070,3071,3072,3073,3074,3075,3076,3077,3078,3079,3080,3081,3082,3083,3084,3085,3086,3087,3088]],[[3089,3090,3091,3092,3093,3094,3095,3096,3097,3098,3099]],[[3100,3101,3102]],[[3103,3104,3105]],[[3106]]]},{"type":"MultiPolygon","properties":{"iso_a2":"CA","name":"Nunavut","postal":"NU","admin":"Canada"},"arcs":[[[3107,3108,3109,3110]],[[3111,3112,3113]],[[3114,3115]],[[3116,3117]],[[3118,3119,3120,3121]],[[3122,3123,3124,3125]],[[3126,3127,3128,3129,3130,3131]],[[3132,3133,3134]],[[3135]],[[3136,3137,3138,3139,3140,3141,3142,3143,3144,3145,3146,3147,3148,3149,3150,3151,3152]],[[3153,3154,3155]],[[3156,3157]],[[3158,3159]],[[3160,3161,3162]],[[3163,3164,3165,3166]],[[3167,3168]],[[3169,3170,3171,3172]],[[3173,3174]],[[3175,3176]],[[3177,3178,3179,3180,3181,3182,3183]],[[-2863,-3001,3184,3185,3186,3187,3188,3189,3190,3191,3192,3193,3194,3195,3196,3197,3198,3199,3200,3201,3202,3203,3204,3205,3206,3207,3208,3209,3210,3211,3212,3213,3214,3215,3216,3217,3218,3219,3220,3221,3222,936,3223,3224,3225,3226,3227,3228,3229,3230,3231,3232,3233,3234,3235,3236,3237,3238,3239,3240,3241,3242,3243,3244,3245,3246,3247,3248,3249,3250,3251,3252,3253,3254,3255,3256,3257,3258,3259,3260,3261,3262,3263,3264,3265,3266,3267,3268,3269,3270,3271,3272,3273,3274,3275,3276,3277,3278,3279,3280]],[[-3028,3281,3282,3283,3284,3285,3286,3287,3288,3289,3290,3291,3292,3293,3294,3295,3296,3297,3298,3299,3300,3301,3302,3303,3304,3305]],[[3306,3307,3308,3309]],[[3310,3311,3312,3313,3314,3315,3316,3317]],[[3318,3319,3320,3321,3322,3323,3324,3325,3326,3327,3328,3329,3330,3331,3332,3333,3334,3335,3336,3337,3338,3339,3340,3341,3342,3343,3344,3345,3346,3347,3348,3349,3350,3351,3352,3353,3354,3355,3356,3357,3358,3359,3360,3361,3362,3363,3364,3365,3366,3367,3368,3369,3370,3371,3372,3373,3374,3375,3376,3377,3378,3379,3380,3381,3382,3383,3384,3385,3386,3387,3388,3389,3390,3391,3392,3393,3394,3395,3396,3397,3398,3399,3400,3401,3402,3403,3404,3405,3406,3407,3408,3409,3410,3411,3412,3413,3414,3415,3416,3417,3418,3419,3420,3421,3422,3423,3424,3425,3426,3427,3428,3429,3430,3431,3432,3433,3434,3435]],[[2091,3436,3437,3438,3439,3440,3441,3442,3443,3444,3445,3446,3447,3448,3449,3450,3451,3452,3453,3454,3455,3456]],[[3457]],[[3458,3459,3460,3461,3462,3463,3464,3465,3466,3467,3468]],[[3469,3470,3471,3472]],[[3473,3474,3475,3476,3477]],[[3478,3479,3480]],[[3481,3482,3483]],[[3484,3485,3486,3487,3488,3489,3490,3491,3492,3493,3494,3495,3496]],[[-3071,3497,3498,3499,3500,3501,3502,3503,3504,3505,3506,3507,3508]],[[3509,3510,3511,3512,3513,3514,3515,3516,3517,3518,3519,3520,3521,3522,3523,3524,3525,3526,3527,3528,3529,3530]],[[3531,3532,3533]],[[3534]],[[3535,3536,3537]],[[3538,3539,3540,3541]],[[3542,3543,3544,3545,3546,3547,3548,3549,3550,3551]],[[3552,3553]],[[3554,3555,3556,3557,3558,3559,3560,3561,3562,3563,3564,3565,3566,3567,3568,3569,3570,3571,3572,3573,3574,3575,3576]],[[3577,3578,3579,3580,3581,3582,3583,1729,3584,3585,3586,3587,3588,3589,3590,3591,3592,3593,3594,3595,3596,3597,3598,3599,3600,3601,3602,3603,3604,3605,3606,3607,3608,3609,3610,3611,3612,3613,3614,3615,3616,3617,3618,3619,3620,3621,3622,3623,3624,3625,3626,3627,3628,3629,3630,3631,3632,3633,3634,3635,3636,3637,3638,3639,3640,3641,3642,3643,3644,3645]]]},{"type":"MultiPolygon","properties":{"iso_a2":"CA","name":"Ontario","postal":"ON","admin":"Canada"},"arcs":[[[3646,3647,3648,3649]],[[3650,3651,3652]],[[3653,3654,3655,3656,3657,3658,3659,3660,3661,3662,3663,3664,3665,3666,3667,3668,3669,3670,3671,3672,3673,3674,3675,3676,3677,3678,3679,3680,3681,3682,3683,3684,3685,3686,3687,3688,3689,3690,3691,3692,3693,3694,3695,3696,3697,3698,3699,3700,3701,3702,-2859,3703,3704,3705,3706,3707,3708,3709,3710,3711,3712,3713,3714,3715]]]},{"type":"Polygon","properties":{"iso_a2":"CA","name":"Prince Edward Island","postal":"PE","admin":"Canada"},"arcs":[[3716,3717,3718,3719,3720,3721,3722,3723,3724,3725]]},{"type":"MultiPolygon","properties":{"iso_a2":"CA","name":"Québec","postal":"QC","admin":"Canada"},"arcs":[[[3726,3727]],[[-2884,3728,3729,3730,3731,3732,3733,3734,3735,3736,3737,3738,3739,3740,3741,3742,3743,3744,3745,3746,3747]],[[1473,3748,3749,3750,3751,3752]],[[-2933,3753,3754,3755,3756,482,3757,3758,3759,3760,3761,3762,3763,3764,3765,3766,3767,3768,3769,3770,3771,3772,3773,-3654,3774,-3653,3775,3776,3777,3778,3779,3780,3781,3782,3783,3784,3785,3786,3787,3788,3789,3790,3791,3792,3793,3794,3795,3796,3797,3798,3799,3800,3801,3802,3803,3804,3805,430,3806,3807,3808,3809,3810,3811,3812,3813,3814]]]},{"type":"Polygon","properties":{"iso_a2":"CA","name":"Saskatchewan","postal":"SK","admin":"Canada"},"arcs":[[3815,3816,-2784,-3002,-2862]]},{"type":"Polygon","properties":{"iso_a2":"CA","name":"Yukon","postal":"YT","admin":"Canada"},"arcs":[[-3003,-2827,3817,3818,3819,3820,3821]]},{"type":"MultiPolygon","properties":{"iso_a2":"US","name":"Alaska","postal":"AK","admin":"United States of America"},"arcs":[[[3822]],[[3823,3824,3825]],[[3826,3827]],[[3828,3829,3830,3831]],[[3832,3833,3834]],[[3835,3836,3837,3838,3839]],[[3840,3841]],[[3842,3843,3844,3845,1551,3846]],[[3847,3848,3849,3850,3851,3852,3853,3854]],[[3855,3856,3857]],[[3858,3859,3860]],[[3861,3862,3863,3864,3865]],[[3866,3867,3868,3869,3870,3871]],[[3872,3873,3874,3875,3876,3877,3878,3879,3880,3881,3882]],[[3883,3884,3885]],[[3886,3887,3888]],[[3889,3890,3891]],[[3892,3893,3894]],[[3895,3896,3897,3898,3899]],[[3900,3901,3902,3903,3904,3905,3906]],[[-3820,-3819,-3818,-2826,-2825,-2824,-2823,-2822,-2821,-2820,-2819,-2818,3907,3908,3909,3910,3911,3912,3913,3914,755,3915,3916,3917,3918,3919,3920,3921,3922,3923,3924,3925,3926,3927,3928,3929,3930,3931,3932,3933,3934,3935,3936,3937,3938,3939,3940,3941,3942,3943,3944,3945,3946,3947,3948,3949,3950,3951,3952,3953,3954,3955,3956,3957,3958,3959,3960,3961,3962,3963,3964,3965,3966,3967,3968,3969,3970,3971,3972,3973,3974,3975,3976,3977,3978,3979,3980,3981,3982,3983,3984,3985,3986,3987,3988,3989,3990,3991,3992,3993,3994,3995,3996,3997,3998,3999,4000,4001,4002,4003,4004,4005,4006,4007,4008,4009,4010,4011,4012,4013,4014,4015,4016,4017,4018,4019,4020,4021,4022,4023,4024,4025,4026,4027,4028,4029,4030,4031]]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Alabama","postal":"AL","admin":"United States of America"},"arcs":[[4032,4033,4034,4035,4036,4037,4038,4039]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Arkansas","postal":"AR","admin":"United States of America"},"arcs":[[4040,4041,4042,4043,4044,4045]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Arizona","postal":"AZ","admin":"United States of America"},"arcs":[[4046,4047,4048,4049,4050,4051,4052,4053]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"California","postal":"CA","admin":"United States of America"},"arcs":[[4054,4055,-4052,4056,4057,4058,4059,4060,4061,4062,4063,4064,4065,4066,4067,4068,4069,4070,4071,4072,4073]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Colorado","postal":"CO","admin":"United States of America"},"arcs":[[4074,4075,4076,4077,4078,4079]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Connecticut","postal":"CT","admin":"United States of America"},"arcs":[[4080,4081,4082,4083,4084]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Delaware","postal":"DE","admin":"United States of America"},"arcs":[[4085,4086,4087,4088,4089]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Florida","postal":"FL","admin":"United States of America"},"arcs":[[4090,4091,4092,4093,4094,4095,4096,4097,4098,4099,4100,4101,4102,4103,4104,4105,4106,4107,4108,4109,4110,622,4111,4112,4113,4114,4115,4116,-4040,4117]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Georgia","postal":"GA","admin":"United States of America"},"arcs":[[4118,4119,4120,-4118,-4039,4121,4122,4123]]},{"type":"MultiPolygon","properties":{"iso_a2":"US","name":"Hawaii","postal":"HI","admin":"United States of America"},"arcs":[[[4124,4125,4126,4127,4128]],[[4129,4130]],[[4131,4132,4133]],[[4134,4135,4136,4137,4138]],[[4139,4140]]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Iowa","postal":"IA","admin":"United States of America"},"arcs":[[4141,4142,4143,4144,4145,4146]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Idaho","postal":"ID","admin":"United States of America"},"arcs":[[4147,4148,4149,4150,4151,-2830,4152]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Illinois","postal":"IL","admin":"United States of America"},"arcs":[[4153,4154,4155,4156,4157,-4142,4158]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Indiana","postal":"IN","admin":"United States of America"},"arcs":[[4159,4160,-4156,4161,4162]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Kansas","postal":"KS","admin":"United States of America"},"arcs":[[4163,-4075,4164,4165]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Kentucky","postal":"KY","admin":"United States of America"},"arcs":[[4166,4167,4168,4169,-4157,-4161,4170]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Louisiana","postal":"LA","admin":"United States of America"},"arcs":[[4171,4172,4173,4174,4175,4176,4177,4178,4179,4180,4181,4182,4183,4184,-4043,4185]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Massachusetts","postal":"MA","admin":"United States of America"},"arcs":[[4186,-4085,4187,4188,4189,4190,4191,4192,4193,4194,4195,4196,4197]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Maryland","postal":"MD","admin":"United States of America"},"arcs":[[-4089,4198,4199,4200,4201,4202,4203,4204,4205,4206,4207,4208,4209,4210,4211]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Maine","postal":"ME","admin":"United States of America"},"arcs":[[-2881,-2880,4212,4213,4214,4215,4216,4217,4218,4219,4220,-3732,-3731,-3730,-3729,-2883,-2882]]},{"type":"MultiPolygon","properties":{"iso_a2":"US","name":"Michigan","postal":"MI","admin":"United States of America"},"arcs":[[[-3672,4221,4222,4223,-4163,4224,4225,4226,4227,4228,4229,4230,4231,4232,4233,4234,4235,4236,4237,4238,4239,4240,4241,4242]],[[4243,4244,4245,4246,4247,4248,4249,4250,4251,4252,4253,4254,4255,4256]],[[4257,4258,4259,4260,4261]]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Minnesota","postal":"MN","admin":"United States of America"},"arcs":[[4262,4263,4264,-4146,4265,4266,-2860,-3703,-3702,-3701,-3700,-3699,-3698]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Missouri","postal":"MO","admin":"United States of America"},"arcs":[[-4158,-4170,4267,-4046,4268,-4166,4269,-4143]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Mississippi","postal":"MS","admin":"United States of America"},"arcs":[[4270,4271,-4186,-4042,4272,-4037]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Montana","postal":"MT","admin":"United States of America"},"arcs":[[4273,4274,-4153,-2829,-2781,-3817,4275]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"North Carolina","postal":"NC","admin":"United States of America"},"arcs":[[4276,-4123,4277,4278,4279,4280,4281,4282,4283,4284,4285,4286,4287,4288,4289,4290]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"North Dakota","postal":"ND","admin":"United States of America"},"arcs":[[4291,-4276,-3816,-2861,-4267]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Nebraska","postal":"NE","admin":"United States of America"},"arcs":[[-4144,-4270,-4165,-4080,4292,4293]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"New Hampshire","postal":"NH","admin":"United States of America"},"arcs":[[4294,-4190,4295,-3734,-3733,-4221]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"New Jersey","postal":"NJ","admin":"United States of America"},"arcs":[[4296,4297,4298,4299,4300,4301]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"New Mexico","postal":"NM","admin":"United States of America"},"arcs":[[4302,4303,4304,4305,-4047,-4077,4306]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Nevada","postal":"NV","admin":"United States of America"},"arcs":[[-4053,-4056,4307,-4150,4308]]},{"type":"MultiPolygon","properties":{"iso_a2":"US","name":"New York","postal":"NY","admin":"United States of America"},"arcs":[[[4309,4310,4311]],[[4312,-4188,-4084,4313,-4298,4314,4315,4316,-3666,4317,4318,4319,4320,4321,4322,-3657,-3656,-3736]]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Ohio","postal":"OH","admin":"United States of America"},"arcs":[[4323,-4171,-4160,-4224,4324,4325,4326,4327,4328]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Oklahoma","postal":"OK","admin":"United States of America"},"arcs":[[-4045,4329,-4307,-4076,-4164,-4269]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Oregon","postal":"OR","admin":"United States of America"},"arcs":[[4330,-4151,-4308,-4055,4331,4332,4333,4334,4335,4336]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Pennsylvania","postal":"PA","admin":"United States of America"},"arcs":[[-4297,4337,-4090,-4212,4338,-4329,4339,-4315]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Rhode Island","postal":"RI","admin":"United States of America"},"arcs":[[4340,4341,-4081,-4187]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"South Carolina","postal":"SC","admin":"United States of America"},"arcs":[[4342,4343,4344,4345,4346,-4124,-4277]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"South Dakota","postal":"SD","admin":"United States of America"},"arcs":[[-4266,-4145,-4294,4347,-4274,-4292]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Tennessee","postal":"TN","admin":"United States of America"},"arcs":[[4348,-4278,-4122,-4038,-4273,-4041,-4268,-4169]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Texas","postal":"TX","admin":"United States of America"},"arcs":[[-4044,-4185,4349,4350,4351,4352,4353,4354,4355,4356,4357,4358,4359,4360,4361,4362,4363,4364,4365,4366,4367,4368,4369,4370,-4303,-4330]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Utah","postal":"UT","admin":"United States of America"},"arcs":[[4371,-4078,-4054,-4309,-4149]]},{"type":"MultiPolygon","properties":{"iso_a2":"US","name":"Virginia","postal":"VA","admin":"United States of America"},"arcs":[[[-4200,4372,4373,4374]],[[-4210,4375,4376,4377,4378,4379,4380,4381,4382,-4279,-4349,-4168,4383]]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Vermont","postal":"VT","admin":"United States of America"},"arcs":[[-4189,-4313,-3735,-4296]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Washington","postal":"WA","admin":"United States of America"},"arcs":[[-4331,4384,4385,4386,4387,4388,4389,4390,4391,4392,4393,4394,4395,4396,-2832,-2831,-4152]]},{"type":"MultiPolygon","properties":{"iso_a2":"US","name":"Wisconsin","postal":"WI","admin":"United States of America"},"arcs":[[[4397,4398,4399]],[[-4244,4400,4401,4402,4403,-4159,-4147,-4265,4404,4405,4406,4407]]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"West Virginia","postal":"WV","admin":"United States of America"},"arcs":[[-4211,-4384,-4167,-4324,-4339]]},{"type":"Polygon","properties":{"iso_a2":"US","name":"Wyoming","postal":"WY","admin":"United States of America"},"arcs":[[-4293,-4079,-4372,-4148,-4275,-4348]]}]},"cities":{"type":"GeometryCollection","geometries":[{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"San Bernardino","adm0name":"United States of America","adm1name":"California","iso_a2":"US"},"coordinates":[174161,706874]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Bridgeport","adm0name":"United States of America","adm1name":"Connecticut","iso_a2":"US"},"coordinates":[296661,748698]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Rochester","adm0name":"United States of America","adm1name":"New York","iso_a2":"US"},"coordinates":[284383,760490]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Manchester","adm0name":"United Kingdom","adm1name":"Manchester","iso_a2":"GB"},"coordinates":[493750,821690]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Gujranwala","adm0name":"Pakistan","adm1name":"Punjab","iso_a2":"PK"},"coordinates":[706063,695262]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Incheon","adm0name":"South Korea","adm1name":"Inch'on-gwangyoksi","iso_a2":"KR"},"coordinates":[851778,726754]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Benin City","adm0name":"Nigeria","adm1name":"Edo","iso_a2":"NG"},"coordinates":[515605,542293]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Xiamen","adm0name":"China","adm1name":"Fujian","iso_a2":"CN"},"coordinates":[827994,649581]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Nanchong","adm0name":"China","adm1name":"Sichuan","iso_a2":"CN"},"coordinates":[794799,687086]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Neijiang","adm0name":"China","adm1name":"Sichuan","iso_a2":"CN"},"coordinates":[791800,679976]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Nanyang","adm0name":"China","adm1name":"Henan","iso_a2":"CN"},"coordinates":[812577,700238]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Jinxi","adm0name":"China","adm1name":"Liaoning","iso_a2":"CN"},"coordinates":[835632,746152]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Yantai","adm0name":"China","adm1name":"Shandong","iso_a2":"CN"},"coordinates":[837216,727075]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Zaozhuang","adm0name":"China","adm1name":"Shandong","iso_a2":"CN"},"coordinates":[826578,711374]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Suzhou","adm0name":"China","adm1name":"Jiangsu","iso_a2":"CN"},"coordinates":[835050,690167]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Xuzhou","adm0name":"China","adm1name":"Jiangsu","iso_a2":"CN"},"coordinates":[825494,707819]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Wuxi","adm0name":"China","adm1name":"Jiangsu","iso_a2":"CN"},"coordinates":[834161,691823]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Jilin","adm0name":"China","adm1name":"Jilin","iso_a2":"CN"},"coordinates":[851522,764516]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Chandigarh","adm0name":"India","adm1name":"Chandigarh","iso_a2":"IN"},"coordinates":[713272,686728]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Jammu","adm0name":"India","adm1name":"Jammu and Kashmir","iso_a2":"IN"},"coordinates":[707901,698528]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Sholapur","adm0name":"India","adm1name":"Maharashtra","iso_a2":"IN"},"coordinates":[710827,609416]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Aurangabad","adm0name":"India","adm1name":"Maharashtra","iso_a2":"IN"},"coordinates":[709217,622600]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Nasik","adm0name":"India","adm1name":"Maharashtra","iso_a2":"IN"},"coordinates":[704938,623220]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Dispur","adm0name":"India","adm1name":"Assam","iso_a2":"IN"},"coordinates":[754907,659606]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Jullundur","adm0name":"India","adm1name":"Punjab","iso_a2":"IN"},"coordinates":[709907,690371]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Allahabad","adm0name":"India","adm1name":"Uttar Pradesh","iso_a2":"IN"},"coordinates":[727327,655536]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Moradabad","adm0name":"India","adm1name":"Uttar Pradesh","iso_a2":"IN"},"coordinates":[718763,675601]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Ghaziabad","adm0name":"India","adm1name":"Uttar Pradesh","iso_a2":"IN"},"coordinates":[715017,674526]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Agra","adm0name":"India","adm1name":"Uttar Pradesh","iso_a2":"IN"},"coordinates":[716703,665699]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Aligarh","adm0name":"India","adm1name":"Uttar Pradesh","iso_a2":"IN"},"coordinates":[716832,669974]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Meerut","adm0name":"India","adm1name":"Uttar Pradesh","iso_a2":"IN"},"coordinates":[715827,676540]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Dhanbad","adm0name":"India","adm1name":"Jharkhand","iso_a2":"IN"},"coordinates":[740049,645733]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Gwalior","adm0name":"India","adm1name":"Madhya Pradesh","iso_a2":"IN"},"coordinates":[717160,660127]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Vadodara","adm0name":"India","adm1name":"Dadra and Nagar Haveli","iso_a2":"IN"},"coordinates":[703271,636904]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Rajkot","adm0name":"India","adm1name":"Dadra and Nagar Haveli","iso_a2":"IN"},"coordinates":[696661,636904]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Durazno","adm0name":"Uruguay","adm1name":"Durazno","iso_a2":"UY"},"coordinates":[343027,306781]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"International Falls","adm0name":"United States of America","adm1name":"Minnesota","iso_a2":"US"},"coordinates":[240525,792652]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"St. Paul","adm0name":"United States of America","adm1name":"Minnesota","iso_a2":"US"},"coordinates":[241431,770986]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Billings","adm0name":"United States of America","adm1name":"Montana","iso_a2":"US"},"coordinates":[198500,775987]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Great Falls","adm0name":"United States of America","adm1name":"Montana","iso_a2":"US"},"coordinates":[190833,786130]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Missoula","adm0name":"United States of America","adm1name":"Montana","iso_a2":"US"},"coordinates":[183352,782409]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Minot","adm0name":"United States of America","adm1name":"North Dakota","iso_a2":"US"},"coordinates":[218622,790468]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Fargo","adm0name":"United States of America","adm1name":"North Dakota","iso_a2":"US"},"coordinates":[231140,782439]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Hilo","adm0name":"United States of America","adm1name":"Hawaii","iso_a2":"US"},"coordinates":[69194,621429]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Olympia","adm0name":"United States of America","adm1name":"Washington","iso_a2":"US"},"coordinates":[158613,783392]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Spokane","adm0name":"United States of America","adm1name":"Washington","iso_a2":"US"},"coordinates":[173833,787136]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Vancouver","adm0name":"United States of America","adm1name":"Washington","iso_a2":"US"},"coordinates":[159333,775052]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Flagstaff","adm0name":"United States of America","adm1name":"Arizona","iso_a2":"US"},"coordinates":[189860,713247]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Tucson","adm0name":"United States of America","adm1name":"Arizona","iso_a2":"US"},"coordinates":[191967,695526]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Santa Barbara","adm0name":"United States of America","adm1name":"California","iso_a2":"US"},"coordinates":[167444,708726]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Fresno","adm0name":"United States of America","adm1name":"California","iso_a2":"US"},"coordinates":[167297,722427]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Eureka","adm0name":"United States of America","adm1name":"California","iso_a2":"US"},"coordinates":[155146,746448]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Colorado Springs","adm0name":"United States of America","adm1name":"Colorado","iso_a2":"US"},"coordinates":[208911,734959]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Reno","adm0name":"United States of America","adm1name":"Nevada","iso_a2":"US"},"coordinates":[167166,738910]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Elko","adm0name":"United States of America","adm1name":"Nevada","iso_a2":"US"},"coordinates":[178438,746627]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Albuquerque","adm0name":"United States of America","adm1name":"New Mexico","iso_a2":"US"},"coordinates":[203773,712695]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Salem","adm0name":"United States of America","adm1name":"Oregon","iso_a2":"US"},"coordinates":[158267,770891]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Casper","adm0name":"United States of America","adm1name":"Wyoming","iso_a2":"US"},"coordinates":[204687,758678]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Topeka","adm0name":"United States of America","adm1name":"Kansas","iso_a2":"US"},"coordinates":[234250,736067]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Kansas City","adm0name":"United States of America","adm1name":"Missouri","iso_a2":"US"},"coordinates":[237205,736416]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Tulsa","adm0name":"United States of America","adm1name":"Oklahoma","iso_a2":"US"},"coordinates":[233527,718708]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Sioux Falls","adm0name":"United States of America","adm1name":"South Dakota","iso_a2":"US"},"coordinates":[231305,762727]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Shreveport","adm0name":"United States of America","adm1name":"Louisiana","iso_a2":"US"},"coordinates":[239528,697262]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Baton Rouge","adm0name":"United States of America","adm1name":"Louisiana","iso_a2":"US"},"coordinates":[246833,685164]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Ft. Worth","adm0name":"United States of America","adm1name":"Texas","iso_a2":"US"},"coordinates":[229610,698684]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Corpus Christi","adm0name":"United States of America","adm1name":"Texas","iso_a2":"US"},"coordinates":[229439,669078]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Austin","adm0name":"United States of America","adm1name":"Texas","iso_a2":"US"},"coordinates":[228487,684044]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Amarillo","adm0name":"United States of America","adm1name":"Texas","iso_a2":"US"},"coordinates":[217139,713436]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"El Paso","adm0name":"United States of America","adm1name":"Texas","iso_a2":"US"},"coordinates":[204133,693008]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Laredo","adm0name":"United States of America","adm1name":"Texas","iso_a2":"US"},"coordinates":[223591,667676]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Merida","adm0name":"Venezuela","adm1name":"Mérida","iso_a2":"VE"},"coordinates":[302417,554483]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Burlington","adm0name":"United States of America","adm1name":"Vermont","iso_a2":"US"},"coordinates":[296631,768212]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Montgomery","adm0name":"United States of America","adm1name":"Alabama","iso_a2":"US"},"coordinates":[260335,696442]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Tallahassee","adm0name":"United States of America","adm1name":"Florida","iso_a2":"US"},"coordinates":[265888,685117]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Orlando","adm0name":"United States of America","adm1name":"Florida","iso_a2":"US"},"coordinates":[273939,673635]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Jacksonville","adm0name":"United States of America","adm1name":"Florida","iso_a2":"US"},"coordinates":[273133,684418]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Savannah","adm0name":"United States of America","adm1name":"Georgia","iso_a2":"US"},"coordinates":[274695,694425]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Columbia","adm0name":"United States of America","adm1name":"South Carolina","iso_a2":"US"},"coordinates":[275277,706385]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Indianapolis","adm0name":"United States of America","adm1name":"Indiana","iso_a2":"US"},"coordinates":[260633,740225]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Wilmington","adm0name":"United States of America","adm1name":"North Carolina","iso_a2":"US"},"coordinates":[283486,707484]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Knoxville","adm0name":"United States of America","adm1name":"Tennessee","iso_a2":"US"},"coordinates":[266888,717820]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Richmond","adm0name":"United States of America","adm1name":"Virginia","iso_a2":"US"},"coordinates":[284855,727192]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Charleston","adm0name":"United States of America","adm1name":"West Virginia","iso_a2":"US"},"coordinates":[273243,731919]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Baltimore","adm0name":"United States of America","adm1name":"Maryland","iso_a2":"US"},"coordinates":[287161,737560]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Syracuse","adm0name":"United States of America","adm1name":"New York","iso_a2":"US"},"coordinates":[288472,759765]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Puerto Ayacucho","adm0name":"Venezuela","adm1name":"Amazonas","iso_a2":"VE"},"coordinates":[312156,538272]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Port-of-Spain","adm0name":"Trinidad and Tobago","adm1name":"Port of Spain","iso_a2":"TT"},"coordinates":[329119,567824]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Augusta","adm0name":"United States of America","adm1name":"Maine","iso_a2":"US"},"coordinates":[306167,767233]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Sault Ste. Marie","adm0name":"United States of America","adm1name":"Michigan","iso_a2":"US"},"coordinates":[265708,780176]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Atakpame","adm0name":"Togo","adm1name":"Plateaux","iso_a2":"TG"},"coordinates":[503110,549328]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Sousse","adm0name":"Tunisia","adm1name":"Sousse","iso_a2":"TN"},"coordinates":[529513,716990]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Taizz","adm0name":"Yemen","adm1name":"Ta`izz","iso_a2":"YE"},"coordinates":[622326,585327]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Sitka","adm0name":"United States of America","adm1name":"Alaska","iso_a2":"US"},"coordinates":[124090,842768]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Lvov","adm0name":"Ukraine","adm1name":"L'viv","iso_a2":"UA"},"coordinates":[566750,799962]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Odessa","adm0name":"Ukraine","adm1name":"Odessa","iso_a2":"UA"},"coordinates":[585299,780156]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Zhytomyr","adm0name":"Ukraine","adm1name":"Zhytomyr","iso_a2":"UA"},"coordinates":[579616,802395]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Dnipropetrovsk","adm0name":"Ukraine","adm1name":"Dnipropetrovs'k","iso_a2":"UA"},"coordinates":[597216,791946]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Donetsk","adm0name":"Ukraine","adm1name":"Donets'k","iso_a2":"UA"},"coordinates":[605077,789102]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Kharkiv","adm0name":"Ukraine","adm1name":"Kharkiv","iso_a2":"UA"},"coordinates":[600689,800951]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Turkmenbasy","adm0name":"Turkmenistan","adm1name":"Balkan","iso_a2":"TM"},"coordinates":[647137,741831]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Bukhara","adm0name":"Uzbekistan","adm1name":"Bukhoro","iso_a2":"UZ"},"coordinates":[678972,740392]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Nukus","adm0name":"Uzbekistan","adm1name":"Karakalpakstan","iso_a2":"UZ"},"coordinates":[665596,756329]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Turkmenabat","adm0name":"Turkmenistan","adm1name":"Chardzhou","iso_a2":"TM"},"coordinates":[676610,736423]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Mary","adm0name":"Turkmenistan","adm1name":"Mary","iso_a2":"TM"},"coordinates":[671759,727476]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Andijon","adm0name":"Uzbekistan","adm1name":"Andijon","iso_a2":"UZ"},"coordinates":[700944,746376]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Haiphong","adm0name":"Vietnam","adm1name":"Qu?ng Ninh","iso_a2":"VN"},"coordinates":[796328,628135]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Da Nang","adm0name":"Vietnam","adm1name":"Ðà N?ng","iso_a2":"VN"},"coordinates":[800693,599864]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Kabwe","adm0name":"Zambia","adm1name":"Central","iso_a2":"ZM"},"coordinates":[579027,419168]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Mufulira","adm0name":"Zambia","adm1name":"Copperbelt","iso_a2":"ZM"},"coordinates":[578499,430365]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Kitwe","adm0name":"Zambia","adm1name":"Copperbelt","iso_a2":"ZM"},"coordinates":[578388,428825]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Livingstone","adm0name":"Zambia","adm1name":"Southern","iso_a2":"ZM"},"coordinates":[571833,398907]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Chitungwiza","adm0name":"Zimbabwe","adm1name":"Harare","iso_a2":"ZW"},"coordinates":[586388,398077]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Douala","adm0name":"Cameroon","adm1name":"Littoral","iso_a2":"CM"},"coordinates":[526967,528784]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Birmingham","adm0name":"United Kingdom","adm1name":"West Midlands","iso_a2":"GB"},"coordinates":[494661,815614]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Belfast","adm0name":"United Kingdom","adm1name":"Belfast","iso_a2":"GB"},"coordinates":[483444,828192]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Izmir","adm0name":"Turkey","adm1name":"Izmir","iso_a2":"TR"},"coordinates":[575416,732442]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Bursa","adm0name":"Turkey","adm1name":"Bursa","iso_a2":"TR"},"coordinates":[580744,742892]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Samsun","adm0name":"Turkey","adm1name":"Samsun","iso_a2":"TR"},"coordinates":[600954,749278]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Konya","adm0name":"Turkey","adm1name":"Konya","iso_a2":"TR"},"coordinates":[590203,729117]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Adana","adm0name":"Turkey","adm1name":"Adana","iso_a2":"TR"},"coordinates":[598105,723904]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Gulu","adm0name":"Uganda","adm1name":"Aswa","iso_a2":"UG"},"coordinates":[589666,521187]},{"type":"Point","properties":{"scalerank":4,"labelrank":0,"name":"Kigali","adm0name":"Rwanda","adm1name":"Kigali City","iso_a2":"RW"},"coordinates":[583496,493155]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Cottica","adm0name":"Suriname","adm1name":"Sipaliwini","iso_a2":"SR"},"coordinates":[349352,527527]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Cordoba","adm0name":"Spain","adm1name":"Andalucía","iso_a2":"ES"},"coordinates":[486749,729135]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Maradi","adm0name":"Niger","adm1name":"Maradi","iso_a2":"NE"},"coordinates":[519712,584648]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Tahoua","adm0name":"Niger","adm1name":"Tahoua","iso_a2":"NE"},"coordinates":[514610,592991]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Constanta","adm0name":"Romania","adm1name":"Constanta","iso_a2":"RO"},"coordinates":[579472,766594]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Luleå","adm0name":"Sweden","adm1name":"Norrbotten","iso_a2":"SE"},"coordinates":[561551,893341]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Sundsvall","adm0name":"Sweden","adm1name":"Västernorrland","iso_a2":"SE"},"coordinates":[548101,874403]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Iasi","adm0name":"Romania","adm1name":"Iasi","iso_a2":"RO"},"coordinates":[576596,784163]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Surat Thani","adm0name":"Thailand","adm1name":"Surat Thani","iso_a2":"TH"},"coordinates":[775944,558926]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Chiang Mai","adm0name":"Thailand","adm1name":"Chiang Mai","iso_a2":"TH"},"coordinates":[774944,616096]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Nakhon Ratchasima","adm0name":"Thailand","adm1name":"Nakhon Ratchasima","iso_a2":"TH"},"coordinates":[783611,593584]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Mbabane","adm0name":"Swaziland","adm1name":"Hhohho","iso_a2":"SZ"},"coordinates":[586481,348805]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Piura","adm0name":"Peru","adm1name":"Piura","iso_a2":"PE"},"coordinates":[276028,473851]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Arequipa","adm0name":"Peru","adm1name":"Arequipa","iso_a2":"PE"},"coordinates":[301300,407449]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Chimbote","adm0name":"Peru","adm1name":"Ancash","iso_a2":"PE"},"coordinates":[281750,450982]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Pucallpa","adm0name":"Peru","adm1name":"Ucayali","iso_a2":"PE"},"coordinates":[292958,455136]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Iquitos","adm0name":"Peru","adm1name":"Loreto","iso_a2":"PE"},"coordinates":[296527,482500]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Huancayo","adm0name":"Peru","adm1name":"Junín","iso_a2":"PE"},"coordinates":[291110,433149]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Ciudad del Este","adm0name":"Paraguay","adm1name":"Alto Paraná","iso_a2":"PY"},"coordinates":[348289,353544]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Ponta Delgada","adm0name":"Portugal","adm1name":"Azores","iso_a2":"PT"},"coordinates":[428704,728355]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Vigo","adm0name":"Spain","adm1name":"Galicia","iso_a2":"ES"},"coordinates":[475750,754847]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Bilbao","adm0name":"Spain","adm1name":"País Vasco","iso_a2":"ES"},"coordinates":[491861,760950]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Kaolack","adm0name":"Senegal","adm1name":"Kaolack","iso_a2":"SM"},"coordinates":[455277,588548]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Kaedi","adm0name":"Senegal","adm1name":"Matam","iso_a2":"SM"},"coordinates":[462500,600397]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Geneina","adm0name":"Sudan","adm1name":"West Darfur","iso_a2":"SD"},"coordinates":[562333,584401]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Medina","adm0name":"Saudi Arabia","adm1name":"Al Madinah","iso_a2":"SA"},"coordinates":[609939,649878]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Tabuk","adm0name":"Saudi Arabia","adm1name":"Tabuk","iso_a2":"SA"},"coordinates":[601541,672875]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Juba","adm0name":"South Sudan","adm1name":"Central Equatoria","iso_a2":"SS"},"coordinates":[587722,533332]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Malakal","adm0name":"South Sudan","adm1name":"Upper Nile","iso_a2":"SS"},"coordinates":[587933,561218]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Omdurman","adm0name":"Sudan","adm1name":"Khartoum","iso_a2":"SD"},"coordinates":[590222,597237]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"El Obeid","adm0name":"Sudan","adm1name":"North Kurdufan","iso_a2":"SD"},"coordinates":[583935,582821]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"The Hague","adm0name":"Netherlands","adm1name":"Zuid-Holland","iso_a2":"NL"},"coordinates":[511860,813263]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Kristiansand","adm0name":"Norway","adm1name":"Vest-Agder","iso_a2":"NO"},"coordinates":[522222,849323]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Ljubljana","adm0name":"Slovenia","adm1name":"Osrednjeslovenska","iso_a2":"SI"},"coordinates":[540318,777570]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Bratislava","adm0name":"Slovakia","adm1name":"Bratislavský","iso_a2":"SK"},"coordinates":[547547,789979]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Hammerfest","adm0name":"Norway","adm1name":"Finnmark","iso_a2":"NO"},"coordinates":[565800,923346]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Doha","adm0name":"Qatar","adm1name":"Ad Dawhah","iso_a2":"QA"},"coordinates":[643147,654526]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Quetta","adm0name":"Pakistan","adm1name":"Baluchistan","iso_a2":"PK"},"coordinates":[686175,683766]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Larkana","adm0name":"Pakistan","adm1name":"Sind","iso_a2":"PK"},"coordinates":[689463,668005]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Springbok","adm0name":"South Africa","adm1name":"Northern Cape","iso_a2":"ZA"},"coordinates":[549675,328958]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Upington","adm0name":"South Africa","adm1name":"Northern Cape","iso_a2":"ZA"},"coordinates":[558972,336107]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Worcester","adm0name":"South Africa","adm1name":"Western Cape","iso_a2":"ZA"},"coordinates":[553999,305418]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"George","adm0name":"South Africa","adm1name":"Western Cape","iso_a2":"ZA"},"coordinates":[562360,303582]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Tete","adm0name":"Mozambique","adm1name":"Tete","iso_a2":"MZ"},"coordinates":[593277,408919]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Pemba","adm0name":"Mozambique","adm1name":"Cabo Delgado","iso_a2":"MZ"},"coordinates":[612589,427799]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Nampula","adm0name":"Mozambique","adm1name":"Nampula","iso_a2":"MZ"},"coordinates":[609147,415044]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Welkom","adm0name":"South Africa","adm1name":"Orange Free State","iso_a2":"ZA"},"coordinates":[574249,339011]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Xai-Xai","adm0name":"Mozambique","adm1name":"Gaza","iso_a2":"MZ"},"coordinates":[593443,356369]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Goroka","adm0name":"Papua New Guinea","adm1name":"Eastern Highlands","iso_a2":"PG"},"coordinates":[903848,468677]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Mt. Hagen","adm0name":"Papua New Guinea","adm1name":"Western Highlands","iso_a2":"PG"},"coordinates":[900601,469980]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Rabaul","adm0name":"Papua New Guinea","adm1name":"East New Britain","iso_a2":"PG"},"coordinates":[922620,479802]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Lae","adm0name":"Papua New Guinea","adm1name":"Morobe","iso_a2":"PG"},"coordinates":[908305,464828]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"David","adm0name":"Panama","adm1name":"Chiriquí","iso_a2":"PA"},"coordinates":[271018,554680]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Oujda","adm0name":"Morocco","adm1name":"Oriental","iso_a2":"MA"},"coordinates":[494694,710237]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Safi","adm0name":"Morocco","adm1name":"Doukkala - Abda","iso_a2":"MA"},"coordinates":[474333,696195]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Podgorica","adm0name":"Montenegro","adm1name":"Podgorica","iso_a2":"ME"},"coordinates":[553517,756305]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Quelimane","adm0name":"Mozambique","adm1name":"Zambezia","iso_a2":"MZ"},"coordinates":[602472,398787]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"East London","adm0name":"South Africa","adm1name":"Eastern Cape","iso_a2":"ZA"},"coordinates":[577416,309388]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Middelburg","adm0name":"South Africa","adm1name":"Eastern Cape","iso_a2":"ZA"},"coordinates":[569472,318097]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Naltchik","adm0name":"Russia","adm1name":"Kabardin-Balkar","iso_a2":"RU"},"coordinates":[621161,762420]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Stavropol","adm0name":"Russia","adm1name":"Stavropol'","iso_a2":"RU"},"coordinates":[616610,771613]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Ugolnye Kopi","adm0name":"Russia","adm1name":"Chukchi Autonomous Okrug","iso_a2":"RU"},"coordinates":[993610,888227]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Kaliningrad","adm0name":"Russia","adm1name":"Kaliningrad","iso_a2":"RU"},"coordinates":[556937,828785]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Pskov","adm0name":"Russia","adm1name":"Pskov","iso_a2":"RU"},"coordinates":[578694,847328]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Bryansk","adm0name":"Russia","adm1name":"Bryansk","iso_a2":"RU"},"coordinates":[595638,820254]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Smolensk","adm0name":"Russia","adm1name":"Smolensk","iso_a2":"RU"},"coordinates":[589020,829274]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Petrozavodsk","adm0name":"Russia","adm1name":"Karelia","iso_a2":"RU"},"coordinates":[595222,871144]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Tver","adm0name":"Russia","adm1name":"Tver'","iso_a2":"RU"},"coordinates":[599693,841581]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Vologda","adm0name":"Russia","adm1name":"Vologda","iso_a2":"RU"},"coordinates":[610888,855504]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Yaroslavl","adm0name":"Russia","adm1name":"Yaroslavl'","iso_a2":"RU"},"coordinates":[610749,846084]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Rostov","adm0name":"Russia","adm1name":"Rostov","iso_a2":"RU"},"coordinates":[610307,784568]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Sochi","adm0name":"Russia","adm1name":"Krasnodar","iso_a2":"RU"},"coordinates":[610360,762964]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Krasnodar","adm0name":"Russia","adm1name":"Krasnodar","iso_a2":"RU"},"coordinates":[608333,771435]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Penza","adm0name":"Russia","adm1name":"Penza","iso_a2":"RU"},"coordinates":[624999,819779]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Ryazan","adm0name":"Russia","adm1name":"Ryazan'","iso_a2":"RU"},"coordinates":[610333,828311]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Voronezh","adm0name":"Russia","adm1name":"Voronezh","iso_a2":"RU"},"coordinates":[609077,811201]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Magnitogorsk","adm0name":"Russia","adm1name":"Chelyabinsk","iso_a2":"RU"},"coordinates":[663833,821217]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Chelyabinsk","adm0name":"Russia","adm1name":"Chelyabinsk","iso_a2":"RU"},"coordinates":[670657,831492]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Vorkuta","adm0name":"Russia","adm1name":"Komi","iso_a2":"RU"},"coordinates":[677805,904617]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Kirov","adm0name":"Russia","adm1name":"Kirov","iso_a2":"RU"},"coordinates":[637971,851831]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Nizhny Tagil","adm0name":"Russia","adm1name":"Sverdlovsk","iso_a2":"RU"},"coordinates":[666597,847862]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Astrakhan","adm0name":"Russia","adm1name":"Astrakhan'","iso_a2":"RU"},"coordinates":[633486,779307]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Orenburg","adm0name":"Russia","adm1name":"Orenburg","iso_a2":"RU"},"coordinates":[653083,811485]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Saratov","adm0name":"Russia","adm1name":"Saratov","iso_a2":"RU"},"coordinates":[627855,810312]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Ulyanovsk","adm0name":"Russia","adm1name":"Ul'yanovsk","iso_a2":"RU"},"coordinates":[634471,826592]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Omsk","adm0name":"Russia","adm1name":"Omsk","iso_a2":"RU"},"coordinates":[703882,830514]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Tyumen","adm0name":"Russia","adm1name":"Tyumen'","iso_a2":"RU"},"coordinates":[682027,843241]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Novokuznetsk","adm0name":"Russia","adm1name":"Kemerovo","iso_a2":"RU"},"coordinates":[741986,823156]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Kemerovo","adm0name":"Russia","adm1name":"Kemerovo","iso_a2":"RU"},"coordinates":[739139,832576]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Groznyy","adm0name":"Russia","adm1name":"Chechnya","iso_a2":"RU"},"coordinates":[626940,761357]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Kandy","adm0name":"Sri Lanka","adm1name":"Kandy","iso_a2":"LK"},"coordinates":[724082,547847]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Sri Jawewardenepura Kotte","adm0name":"Sri Lanka","adm1name":"Colombo","iso_a2":"LK"},"coordinates":[722082,545596]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Daejeon","adm0name":"South Korea","adm1name":"Daejeon","iso_a2":"KR"},"coordinates":[853952,719997]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Gwangju","adm0name":"South Korea","adm1name":"Kwangju-gwangyoksi","iso_a2":"KR"},"coordinates":[852523,713097]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Busan","adm0name":"South Korea","adm1name":"Busan","iso_a2":"KR"},"coordinates":[858355,712648]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Zamboanga","adm0name":"Philippines","adm1name":"Zamboanga del Sur","iso_a2":"PH"},"coordinates":[839105,545725]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Laoag","adm0name":"Philippines","adm1name":"Ilocos Norte","iso_a2":"PH"},"coordinates":[834981,612535]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Baguio City","adm0name":"Philippines","adm1name":"Benguet","iso_a2":"PH"},"coordinates":[834915,602056]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"General Santos","adm0name":"Philippines","adm1name":"South Cotabato","iso_a2":"PH"},"coordinates":[847707,540920]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Ust-Ulimsk","adm0name":"Russia","adm1name":"Irkutsk","iso_a2":"RU"},"coordinates":[785092,848276]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Angarsk","adm0name":"Russia","adm1name":"Irkutsk","iso_a2":"RU"},"coordinates":[788666,816107]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Abakan","adm0name":"Russia","adm1name":"Krasnoyarsk","iso_a2":"RU"},"coordinates":[754013,822882]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Norilsk","adm0name":"Russia","adm1name":"Taymyr","iso_a2":"RU"},"coordinates":[745068,915519]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Khatanga","adm0name":"Russia","adm1name":"Taymyr","iso_a2":"RU"},"coordinates":[784624,931522]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Kyzyl","adm0name":"Russia","adm1name":"Tuva","iso_a2":"RU"},"coordinates":[762175,811051]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Ulan Ude","adm0name":"Russia","adm1name":"Buryat","iso_a2":"RU"},"coordinates":[798958,811752]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Blagoveshchensk","adm0name":"Russia","adm1name":"Amur","iso_a2":"RU"},"coordinates":[854259,802519]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Bukachacha","adm0name":"Russia","adm1name":"Chita","iso_a2":"RU"},"coordinates":[824768,818614]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Dalnegorsk","adm0name":"Russia","adm1name":"Primor'ye","iso_a2":"RU"},"coordinates":[876436,768575]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Ambarchik","adm0name":"Russia","adm1name":"Sakha (Yakutia)","iso_a2":"RU"},"coordinates":[950925,917361]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Batagay","adm0name":"Russia","adm1name":"Sakha (Yakutia)","iso_a2":"RU"},"coordinates":[873985,905542]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Chokurdakh","adm0name":"Russia","adm1name":"Sakha (Yakutia)","iso_a2":"RU"},"coordinates":[910817,923092]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Ust Nera","adm0name":"Russia","adm1name":"Sakha (Yakutia)","iso_a2":"RU"},"coordinates":[897777,887239]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Lensk","adm0name":"Russia","adm1name":"Sakha (Yakutia)","iso_a2":"RU"},"coordinates":[819296,864481]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Aldan","adm0name":"Russia","adm1name":"Sakha (Yakutia)","iso_a2":"RU"},"coordinates":[848303,851908]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Mirnyy","adm0name":"Russia","adm1name":"Sakha (Yakutia)","iso_a2":"RU"},"coordinates":[816558,875232]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Zhigansk","adm0name":"Russia","adm1name":"Sakha (Yakutia)","iso_a2":"RU"},"coordinates":[842697,900291]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Okhotsk","adm0name":"Russia","adm1name":"Khabarovsk","iso_a2":"RU"},"coordinates":[897824,856529]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Khabarovsk","adm0name":"Russia","adm1name":"Khabarovsk","iso_a2":"RU"},"coordinates":[875333,791786]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Okha","adm0name":"Russia","adm1name":"Sakhalin","iso_a2":"RU"},"coordinates":[897076,822113]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Yuzhno Sakhalinsk","adm0name":"Russia","adm1name":"Sakhalin","iso_a2":"RU"},"coordinates":[896500,782959]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Mexicali","adm0name":"Mexico","adm1name":"Baja California","iso_a2":"MX"},"coordinates":[179216,698162]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"La Paz","adm0name":"Mexico","adm1name":"Baja California Sur","iso_a2":"MX"},"coordinates":[193556,647733]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Torreon","adm0name":"Mexico","adm1name":"Coahuila","iso_a2":"MX"},"coordinates":[212716,656217]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Culiacan","adm0name":"Mexico","adm1name":"Sinaloa","iso_a2":"MX"},"coordinates":[201716,651833]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Nogales","adm0name":"Mexico","adm1name":"Sonora","iso_a2":"MX"},"coordinates":[191820,690182]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Hermosillo","adm0name":"Mexico","adm1name":"Sonora","iso_a2":"MX"},"coordinates":[191794,677112]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Guaymas","adm0name":"Mexico","adm1name":"Sonora","iso_a2":"MX"},"coordinates":[191972,670187]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"San Luis Potosi","adm0name":"Mexico","adm1name":"San Luis Potosí","iso_a2":"MX"},"coordinates":[219439,636074]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Matamoros","adm0name":"Mexico","adm1name":"Tamaulipas","iso_a2":"MX"},"coordinates":[229166,658042]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Nuevo Laredo","adm0name":"Mexico","adm1name":"Tamaulipas","iso_a2":"MX"},"coordinates":[223472,667639]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Colima","adm0name":"Mexico","adm1name":"Colima","iso_a2":"MX"},"coordinates":[211889,618644]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Campeche","adm0name":"Mexico","adm1name":"Campeche","iso_a2":"MX"},"coordinates":[248611,622199]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Oaxaca","adm0name":"Mexico","adm1name":"Oaxaca","iso_a2":"MX"},"coordinates":[231472,605923]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Leon","adm0name":"Mexico","adm1name":"Guanajuato","iso_a2":"MX"},"coordinates":[217495,630031]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Maiduguri","adm0name":"Nigeria","adm1name":"Borno","iso_a2":"NG"},"coordinates":[536550,574933]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Port Harcourt","adm0name":"Nigeria","adm1name":"Rivers","iso_a2":"NG"},"coordinates":[519466,533225]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Makurdi","adm0name":"Nigeria","adm1name":"Benue","iso_a2":"NG"},"coordinates":[523694,550513]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Ibadan","adm0name":"Nigeria","adm1name":"Oyo","iso_a2":"NG"},"coordinates":[510910,548451]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Ogbomosho","adm0name":"Nigeria","adm1name":"Oyo","iso_a2":"NG"},"coordinates":[511772,552894]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Warri","adm0name":"Nigeria","adm1name":"Delta","iso_a2":"NG"},"coordinates":[515999,537420]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Kaduna","adm0name":"Nigeria","adm1name":"Kaduna","iso_a2":"NG"},"coordinates":[520661,567054]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Gdansk","adm0name":"Poland","adm1name":"Pomeranian","iso_a2":"PL"},"coordinates":[551777,826770]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Kraków","adm0name":"Poland","adm1name":"Lesser Poland","iso_a2":"PL"},"coordinates":[555438,801306]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Dalandzadgad","adm0name":"Mongolia","adm1name":"Ömnögovi","iso_a2":"MN"},"coordinates":[790111,762926]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Wonsan","adm0name":"North Korea","adm1name":"Kangwon-do","iso_a2":"KP"},"coordinates":[853974,736721]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Sinuiju","adm0name":"North Korea","adm1name":"P'yongan-bukto","iso_a2":"KP"},"coordinates":[845613,742204]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Dund-Us","adm0name":"Mongolia","adm1name":"Hovd","iso_a2":"MN"},"coordinates":[754536,789189]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Choybalsan","adm0name":"Mongolia","adm1name":"Dornod","iso_a2":"MN"},"coordinates":[818071,789486]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Lüderitz","adm0name":"Namibia","adm1name":"Karas","iso_a2":"NA"},"coordinates":[542109,346842]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Walvis Bay","adm0name":"Namibia","adm1name":"Erongo","iso_a2":"NA"},"coordinates":[540292,368707]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Mwanza","adm0name":"Tanzania","adm1name":"Mwanza","iso_a2":"TZ"},"coordinates":[591472,489788]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Morogoro","adm0name":"Tanzania","adm1name":"Morogoro","iso_a2":"TZ"},"coordinates":[604610,464312]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Dodoma","adm0name":"Tanzania","adm1name":"Dodoma","iso_a2":"TZ"},"coordinates":[599305,468084]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Arusha","adm0name":"Tanzania","adm1name":"Arusha","iso_a2":"TZ"},"coordinates":[601861,484811]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Napier","adm0name":"New Zealand","adm1name":"Gisborne","iso_a2":"NZ"},"coordinates":[991429,270760]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Manukau","adm0name":"New Zealand","adm1name":"Auckland","iso_a2":"NZ"},"coordinates":[985790,285513]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Hamilton","adm0name":"New Zealand","adm1name":"Auckland","iso_a2":"NZ"},"coordinates":[986944,280950]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Blenheim","adm0name":"New Zealand","adm1name":"Marlborough","iso_a2":"NZ"},"coordinates":[983220,258728]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Dunedin","adm0name":"New Zealand","adm1name":"Otago","iso_a2":"NZ"},"coordinates":[973555,232904]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Bern","adm0name":"Switzerland","adm1name":"Bern","iso_a2":"CH"},"coordinates":[520741,782673]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Malmö","adm0name":"Sweden","adm1name":"Skåne","iso_a2":"SE"},"coordinates":[536203,834018]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Laayoune","adm0name":"Morocco","adm1name":"Laâyoune - Boujdour - Sakia El Hamra","iso_a2":"MA"},"coordinates":[463333,665566]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Ternate","adm0name":"Indonesia","adm1name":"Maluku Utara","iso_a2":"ID"},"coordinates":[853785,509415]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Ambon","adm0name":"Indonesia","adm1name":"Maluku","iso_a2":"ID"},"coordinates":[856110,482698]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Raba","adm0name":"Indonesia","adm1name":"Nusa Tenggara Barat","iso_a2":"ID"},"coordinates":[829907,454656]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Jayapura","adm0name":"Indonesia","adm1name":"Papua","iso_a2":"ID"},"coordinates":[890832,489710]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Florence","adm0name":"Italy","adm1name":"Toscana","iso_a2":"IT"},"coordinates":[531250,764090]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Catania","adm0name":"Italy","adm1name":"Sicily","iso_a2":"IT"},"coordinates":[541888,726884]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Pristina","adm0name":"Kosovo","adm1name":"Pristina","iso_a2":"-99"},"coordinates":[558793,757494]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Meru","adm0name":"Kenya","adm1name":"Eastern","iso_a2":"KE"},"coordinates":[604555,505072]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Eldoret","adm0name":"Kenya","adm1name":"Rift Valley","iso_a2":"KE"},"coordinates":[597971,507798]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Banda Aceh","adm0name":"Indonesia","adm1name":"Aceh","iso_a2":"ID"},"coordinates":[764777,537597]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"George Town","adm0name":"Malaysia","adm1name":"Pulau Pinang","iso_a2":"MY"},"coordinates":[778692,536790]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Zhangye","adm0name":"China","adm1name":"Gansu","iso_a2":"CN"},"coordinates":[779027,735356]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Wuwei","adm0name":"China","adm1name":"Gansu","iso_a2":"CN"},"coordinates":[785113,729420]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Dunhuang","adm0name":"China","adm1name":"Gansu","iso_a2":"CN"},"coordinates":[762949,742540]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Tianshui","adm0name":"China","adm1name":"Gansu","iso_a2":"CN"},"coordinates":[794216,709715]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Dulan","adm0name":"China","adm1name":"Gansu","iso_a2":"CN"},"coordinates":[772962,718985]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Golmud","adm0name":"China","adm1name":"Gansu","iso_a2":"CN"},"coordinates":[763564,720465]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Yulin","adm0name":"China","adm1name":"Guangxi","iso_a2":"CN"},"coordinates":[805966,638799]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Bose","adm0name":"China","adm1name":"Guangxi","iso_a2":"CN"},"coordinates":[796147,646309]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Wuzhou","adm0name":"China","adm1name":"Guangxi","iso_a2":"CN"},"coordinates":[809222,643823]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Lupanshui","adm0name":"China","adm1name":"Guizhou","iso_a2":"CN"},"coordinates":[791198,662286]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Quanzhou","adm0name":"China","adm1name":"Fujian","iso_a2":"CN"},"coordinates":[829382,652248]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Hefei","adm0name":"China","adm1name":"Anhui","iso_a2":"CN"},"coordinates":[825772,693423]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Suzhou","adm0name":"China","adm1name":"Anhui","iso_a2":"CN"},"coordinates":[824935,704004]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Zhanjiang","adm0name":"China","adm1name":"Guangdong","iso_a2":"CN"},"coordinates":[806605,630327]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Shaoguan","adm0name":"China","adm1name":"Guangdong","iso_a2":"CN"},"coordinates":[815499,651643]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Balikpapan","adm0name":"Indonesia","adm1name":"Kalimantan Timur","iso_a2":"ID"},"coordinates":[824527,497311]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Kuching","adm0name":"Malaysia","adm1name":"Sarawak","iso_a2":"MY"},"coordinates":[806472,513781]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Antsiranana","adm0name":"Madagascar","adm1name":"Antsiranana","iso_a2":"MG"},"coordinates":[636976,431985]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Fianarantsoa","adm0name":"Madagascar","adm1name":"Fianarantsoa","iso_a2":"MG"},"coordinates":[630786,377736]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Mahajanga","adm0name":"Madagascar","adm1name":"Mahajanga","iso_a2":"MG"},"coordinates":[628736,411881]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Toliara","adm0name":"Madagascar","adm1name":"Toliary","iso_a2":"MG"},"coordinates":[621360,366340]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Surakarta","adm0name":"Indonesia","adm1name":"Jawa Tengah","iso_a2":"ID"},"coordinates":[807846,459899]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Bandar Lampung","adm0name":"Indonesia","adm1name":"Lampung","iso_a2":"ID"},"coordinates":[792411,472559]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Tanjungpandan","adm0name":"Indonesia","adm1name":"Bangka-Belitung","iso_a2":"ID"},"coordinates":[799027,488425]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Malang","adm0name":"Indonesia","adm1name":"Jawa Timur","iso_a2":"ID"},"coordinates":[812800,457451]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Kupang","adm0name":"Indonesia","adm1name":"Nusa Tenggara Timur","iso_a2":"ID"},"coordinates":[843285,444414]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Parepare","adm0name":"Indonesia","adm1name":"Sulawesi Selatan","iso_a2":"ID"},"coordinates":[832314,480920]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Cuenca","adm0name":"Ecuador","adm1name":"Azuay","iso_a2":"EC"},"coordinates":[280555,487536]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Santa Cruz","adm0name":"Ecuador","adm1name":"Galápagos","iso_a2":"EC"},"coordinates":[249027,501557]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Puerto Limon","adm0name":"Costa Rica","adm1name":"Limón","iso_a2":"CR"},"coordinates":[269351,563962]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Santiago de Cuba","adm0name":"Cuba","adm1name":"Santiago de Cuba","iso_a2":"CU"},"coordinates":[289385,623354]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Santiago","adm0name":"Dominican Republic","adm1name":"Santiago","iso_a2":"DO"},"coordinates":[303694,620244]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Manizales","adm0name":"Colombia","adm1name":"Caldas","iso_a2":"CO"},"coordinates":[290222,534695]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Pasto","adm0name":"Colombia","adm1name":"Nariño","iso_a2":"CO"},"coordinates":[285330,511907]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Barranquilla","adm0name":"Colombia","adm1name":"Atlántico","iso_a2":"CO"},"coordinates":[292217,569660]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Roseau","adm0name":"Dominica","adm1name":"Saint George","iso_a2":"DM"},"coordinates":[329480,595367]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Mbandaka","adm0name":"Congo (Kinshasa)","adm1name":"Équateur","iso_a2":"CD"},"coordinates":[550722,504955]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Moundou","adm0name":"Chad","adm1name":"Logone Oriental","iso_a2":"TD"},"coordinates":[544694,555371]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Suez","adm0name":"Egypt","adm1name":"As Suways","iso_a2":"EG"},"coordinates":[590416,682480]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Bur Said","adm0name":"Egypt","adm1name":"Bur Sa`id","iso_a2":"EG"},"coordinates":[589694,689915]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"El Faiyum","adm0name":"Egypt","adm1name":"Al Fayyum","iso_a2":"EG"},"coordinates":[585666,678363]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Aswan","adm0name":"Egypt","adm1name":"Aswan","iso_a2":"EG"},"coordinates":[591385,647422]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Asyut","adm0name":"Egypt","adm1name":"Asyut","iso_a2":"EG"},"coordinates":[586610,665803]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Kisangani","adm0name":"Congo (Kinshasa)","adm1name":"Orientale","iso_a2":"CD"},"coordinates":[570055,507798]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Assab","adm0name":"Eritrea","adm1name":"Debubawi Keyih Bahri","iso_a2":"ER"},"coordinates":[618694,581794]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Djibouti","adm0name":"Djibouti","adm1name":"Djibouti","iso_a2":"DJ"},"coordinates":[619855,573411]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Dresden","adm0name":"Germany","adm1name":"Sachsen","iso_a2":"DE"},"coordinates":[538194,807160]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Xigaze","adm0name":"China","adm1name":"Xizang","iso_a2":"CN"},"coordinates":[746897,678007]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Shache","adm0name":"China","adm1name":"Xinjiang Uygur","iso_a2":"CN"},"coordinates":[714583,732371]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Yining","adm0name":"China","adm1name":"Xinjiang Uygur","iso_a2":"CN"},"coordinates":[725971,764800]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Altay","adm0name":"China","adm1name":"Xinjiang Uygur","iso_a2":"CN"},"coordinates":[744768,788301]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Putrajaya","adm0name":"Malaysia","adm1name":"Selangor","iso_a2":"MY"},"coordinates":[782504,521981]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Shizuishan","adm0name":"China","adm1name":"Ningxia Hui","iso_a2":"CN"},"coordinates":[796580,737152]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Yulin","adm0name":"China","adm1name":"Shaanxi","iso_a2":"CN"},"coordinates":[804814,731525]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Ankang","adm0name":"China","adm1name":"Shaanxi","iso_a2":"CN"},"coordinates":[802832,698328]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Houma","adm0name":"China","adm1name":"Shanxi","iso_a2":"CN"},"coordinates":[808916,715746]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Yueyang","adm0name":"China","adm1name":"Hunan","iso_a2":"CN"},"coordinates":[814160,678790]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Hengyang","adm0name":"China","adm1name":"Hunan","iso_a2":"CN"},"coordinates":[812744,663978]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Mianyang","adm0name":"China","adm1name":"Sichuan","iso_a2":"CN"},"coordinates":[791022,691171]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Xichang","adm0name":"China","adm1name":"Sichuan","iso_a2":"CN"},"coordinates":[784166,669891]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Baoshan","adm0name":"China","adm1name":"Yunnan","iso_a2":"CN"},"coordinates":[775415,653540]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Gejiu","adm0name":"China","adm1name":"Yunnan","iso_a2":"CN"},"coordinates":[786527,643230]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Shijianzhuang","adm0name":"China","adm1name":"Hebei","iso_a2":"CN"},"coordinates":[817993,730154]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Handan","adm0name":"China","adm1name":"Hebei","iso_a2":"CN"},"coordinates":[817993,721445]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Anshan","adm0name":"China","adm1name":"Liaoning","iso_a2":"CN"},"coordinates":[841494,748312]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Dalian","adm0name":"China","adm1name":"Liaoning","iso_a2":"CN"},"coordinates":[837855,735325]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Qingdao","adm0name":"China","adm1name":"Shandong","iso_a2":"CN"},"coordinates":[834244,718542]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Linyi","adm0name":"China","adm1name":"Shandong","iso_a2":"CN"},"coordinates":[828688,712559]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Huaiyin","adm0name":"China","adm1name":"Jiangsu","iso_a2":"CN"},"coordinates":[830633,703671]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Wenzhou","adm0name":"China","adm1name":"Zhejiang","iso_a2":"CN"},"coordinates":[835133,670731]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Ningbo","adm0name":"China","adm1name":"Zhejiang","iso_a2":"CN"},"coordinates":[837632,681751]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Fukuoka","adm0name":"Japan","adm1name":"Fukuoka","iso_a2":"JP"},"coordinates":[862243,703760]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Miyazaki","adm0name":"Japan","adm1name":"Miyazaki","iso_a2":"JP"},"coordinates":[865050,693815]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Naha","adm0name":"Japan","adm1name":"Okinawa","iso_a2":"JP"},"coordinates":[854647,659980]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Kochi","adm0name":"Japan","adm1name":"Kochi","iso_a2":"JP"},"coordinates":[870937,703556]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Gorontalo","adm0name":"Indonesia","adm1name":"Gorontalo","iso_a2":"ID"},"coordinates":[841861,507976]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Tongliao","adm0name":"China","adm1name":"Nei Mongol","iso_a2":"CN"},"coordinates":[839633,763153]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Hohhot","adm0name":"China","adm1name":"Nei Mongol","iso_a2":"CN"},"coordinates":[810161,746565]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Chifeng","adm0name":"China","adm1name":"Nei Mongol","iso_a2":"CN"},"coordinates":[830410,755155]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Ulanhot","adm0name":"China","adm1name":"Nei Mongol","iso_a2":"CN"},"coordinates":[839110,777716]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Hailar","adm0name":"China","adm1name":"Nei Mongol","iso_a2":"CN"},"coordinates":[832499,796200]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Jiamusi","adm0name":"China","adm1name":"Heilongjiang","iso_a2":"CN"},"coordinates":[862077,782171]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Beian","adm0name":"China","adm1name":"Heilongjiang","iso_a2":"CN"},"coordinates":[851338,790507]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Daqing","adm0name":"China","adm1name":"Heilongjiang","iso_a2":"CN"},"coordinates":[847216,780690]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Jixi","adm0name":"China","adm1name":"Heilongjiang","iso_a2":"CN"},"coordinates":[863800,773106]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Nagoya","adm0name":"Japan","adm1name":"Aichi","iso_a2":"JP"},"coordinates":[880313,713003]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Nagano","adm0name":"Japan","adm1name":"Nagano","iso_a2":"JP"},"coordinates":[883805,721849]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Kushiro","adm0name":"Japan","adm1name":"Hokkaido","iso_a2":"JP"},"coordinates":[901040,759320]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Hakodate","adm0name":"Japan","adm1name":"Hokkaido","iso_a2":"JP"},"coordinates":[890943,752329]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Kyoto","adm0name":"Japan","adm1name":"Kyoto","iso_a2":"JP"},"coordinates":[877077,712262]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Sendai","adm0name":"Japan","adm1name":"Miyagi","iso_a2":"JP"},"coordinates":[891720,731559]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Sakata","adm0name":"Japan","adm1name":"Yamagata","iso_a2":"JP"},"coordinates":[888471,735297]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Bandundu","adm0name":"Congo (Kinshasa)","adm1name":"Bandundu","iso_a2":"CD"},"coordinates":[548277,485107]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Kananga","adm0name":"Congo (Kinshasa)","adm1name":"Kasaï-Occidental","iso_a2":"CD"},"coordinates":[562216,469833]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Kasongo","adm0name":"Congo (Kinshasa)","adm1name":"Maniema","iso_a2":"CD"},"coordinates":[574055,478353]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Mbuji-Mayi","adm0name":"Congo (Kinshasa)","adm1name":"Kasaï-Oriental","iso_a2":"CD"},"coordinates":[565549,468293]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Kalemie","adm0name":"Congo (Kinshasa)","adm1name":"Katanga","iso_a2":"CD"},"coordinates":[581110,469566]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Butembo","adm0name":"Congo (Kinshasa)","adm1name":"Nord-Kivu","iso_a2":"CD"},"coordinates":[581332,505487]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Goma","adm0name":"Congo (Kinshasa)","adm1name":"Nord-Kivu","iso_a2":"CD"},"coordinates":[581171,494771]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Mzuzu","adm0name":"Malawi","adm1name":"Mzimba","iso_a2":"MW"},"coordinates":[594500,436823]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Blantyre","adm0name":"Malawi","adm1name":"Blantyre","iso_a2":"MW"},"coordinates":[597193,411170]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Quetzaltenango","adm0name":"Guatemala","adm1name":"Quezaltenango","iso_a2":"GT"},"coordinates":[245778,592576]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Banjul","adm0name":"The Gambia","adm1name":"Banjul","iso_a2":"GM"},"coordinates":[453912,584424]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Faridabad","adm0name":"India","adm1name":"Haryana","iso_a2":"IN"},"coordinates":[714762,673180]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Srinagar","adm0name":"India","adm1name":"Jammu and Kashmir","iso_a2":"IN"},"coordinates":[707813,706752]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Vijayawada","adm0name":"India","adm1name":"Andhra Pradesh","iso_a2":"IN"},"coordinates":[723966,602600]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Thiruvananthapuram","adm0name":"India","adm1name":"Kerala","iso_a2":"IN"},"coordinates":[713744,555086]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Kochi","adm0name":"India","adm1name":"Kerala","iso_a2":"IN"},"coordinates":[711727,564062]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Cuttack","adm0name":"India","adm1name":"Orissa","iso_a2":"IN"},"coordinates":[738583,625991]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Hubli","adm0name":"India","adm1name":"Karnataka","iso_a2":"IN"},"coordinates":[708674,595728]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Mangalore","adm0name":"India","adm1name":"Karnataka","iso_a2":"IN"},"coordinates":[707916,581143]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Mysore","adm0name":"India","adm1name":"Karnataka","iso_a2":"IN"},"coordinates":[712938,577658]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Gulbarga","adm0name":"India","adm1name":"Karnataka","iso_a2":"IN"},"coordinates":[713388,607506]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Kolhapur","adm0name":"India","adm1name":"Maharashtra","iso_a2":"IN"},"coordinates":[706166,603655]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Nanded","adm0name":"India","adm1name":"Maharashtra","iso_a2":"IN"},"coordinates":[714721,618289]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Akola","adm0name":"India","adm1name":"Maharashtra","iso_a2":"IN"},"coordinates":[713916,627412]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Guwahati","adm0name":"India","adm1name":"Assam","iso_a2":"IN"},"coordinates":[754911,659712]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Kayes","adm0name":"Congo (Brazzaville)","adm1name":"Bouenza","iso_a2":"CG"},"coordinates":[536888,479953]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Franceville","adm0name":"Gabon","adm1name":"Haut-Ogooué","iso_a2":"GA"},"coordinates":[537731,495041]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Bordeaux","adm0name":"France","adm1name":"Aquitaine","iso_a2":"FR"},"coordinates":[498342,770440]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Marseille","adm0name":"France","adm1name":"Provence-Alpes-Côte-d'Azur","iso_a2":"FR"},"coordinates":[514925,761198]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Le Havre","adm0name":"France","adm1name":"Haute-Normandie","iso_a2":"FR"},"coordinates":[500291,798007]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Gao","adm0name":"Mali","adm1name":"Gao","iso_a2":"ML"},"coordinates":[499860,601088]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Coihaique","adm0name":"Chile","adm1name":"Aisén del General Carlos Ibáñez del Campo","iso_a2":"CL"},"coordinates":[299806,234740]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Arica","adm0name":"Chile","adm1name":"Arica y Parinacota","iso_a2":"CL"},"coordinates":[304750,395115]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Copiapo","adm0name":"Chile","adm1name":"Atacama","iso_a2":"CL"},"coordinates":[304610,342624]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"La Serena","adm0name":"Chile","adm1name":"Coquimbo","iso_a2":"CL"},"coordinates":[302083,327576]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Los Angeles","adm0name":"Chile","adm1name":"Bío-Bío","iso_a2":"CL"},"coordinates":[299000,282787]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Narsarsuaq","adm0name":"Greenland","adm1name":"Kommune Kujalleq","iso_a2":"GL"},"coordinates":[373843,867096]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Sisimiut","adm0name":"Greenland","adm1name":"Qeqqata Kommunia","iso_a2":"GL"},"coordinates":[350925,901360]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Upernavik","adm0name":"Greenland","adm1name":"Qaasuitsup Kommunia","iso_a2":"GL"},"coordinates":[344051,935480]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Qaanaaq","adm0name":"Greenland","adm1name":"Qaasuitsup Kommunia","iso_a2":"GL"},"coordinates":[307410,963764]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Nouadhibou","adm0name":"Mauritania","adm1name":"Dakhlet Nouadhibou","iso_a2":"MR"},"coordinates":[452622,628538]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Kayes","adm0name":"Mali","adm1name":"Kayes","iso_a2":"ML"},"coordinates":[468221,590325]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Ayoun el Atrous","adm0name":"Mauritania","adm1name":"Hodh el Gharbi","iso_a2":"MR"},"coordinates":[473287,603457]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Segou","adm0name":"Mali","adm1name":"Ségou","iso_a2":"ML"},"coordinates":[482611,584342]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Skopje","adm0name":"Macedonia","adm1name":"Centar","iso_a2":"MK"},"coordinates":[559537,753544]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Al Jawf","adm0name":"Libya","adm1name":"Al Kufrah","iso_a2":"LY"},"coordinates":[564694,648089]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Tmassah","adm0name":"Libya","adm1name":"Murzuq","iso_a2":"LY"},"coordinates":[543888,660925]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Misratah","adm0name":"Libya","adm1name":"Misratah","iso_a2":"LY"},"coordinates":[541944,696550]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Zuwarah","adm0name":"Libya","adm1name":"An Nuqat al Khams","iso_a2":"LY"},"coordinates":[533553,699835]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Kirkuk","adm0name":"Iraq","adm1name":"At-Ta'mim","iso_a2":"IQ"},"coordinates":[623311,714871]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Mosul","adm0name":"Iraq","adm1name":"Ninawa","iso_a2":"IQ"},"coordinates":[619841,720053]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"An Najaf","adm0name":"Iraq","adm1name":"An-Najaf","iso_a2":"IQ"},"coordinates":[623153,694302]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Bahir Dar","adm0name":"Ethiopia","adm1name":"Amhara","iso_a2":"ET"},"coordinates":[603842,573441]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Mekele","adm0name":"Ethiopia","adm1name":"Tigray","iso_a2":"ET"},"coordinates":[609638,584697]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Dire Dawa","adm0name":"Ethiopia","adm1name":"Dire Dawa","iso_a2":"ET"},"coordinates":[616277,561532]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Rovaniemi","adm0name":"Finland","adm1name":"Lapland","iso_a2":"FI"},"coordinates":[571433,898693]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Vaasa","adm0name":"Finland","adm1name":"Western Finland","iso_a2":"FI"},"coordinates":[560000,878550]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Tampere","adm0name":"Finland","adm1name":"Pirkanmaa","iso_a2":"FI"},"coordinates":[565972,869071]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Aqtobe","adm0name":"Kazakhstan","adm1name":"Aqtöbe","iso_a2":"KZ"},"coordinates":[658805,802598]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Rudny","adm0name":"Kazakhstan","adm1name":"Qostanay","iso_a2":"KZ"},"coordinates":[675360,818432]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Qyzylorda","adm0name":"Kazakhstan","adm1name":"Qyzylorda","iso_a2":"KZ"},"coordinates":[681846,770133]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Atyrau","adm0name":"Kazakhstan","adm1name":"Atyrau","iso_a2":"KZ"},"coordinates":[644221,783834]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Ekibastuz","adm0name":"Kazakhstan","adm1name":"Pavlodar","iso_a2":"KZ"},"coordinates":[709222,811189]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Pavlodar","adm0name":"Kazakhstan","adm1name":"Pavlodar","iso_a2":"KZ"},"coordinates":[713750,814566]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Semey","adm0name":"Kazakhstan","adm1name":"East Kazakhstan","iso_a2":"KZ"},"coordinates":[722985,803517]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Oskemen","adm0name":"Kazakhstan","adm1name":"East Kazakhstan","iso_a2":"KZ"},"coordinates":[729486,800881]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Yazd","adm0name":"Iran","adm1name":"Yazd","iso_a2":"IR"},"coordinates":[651028,693826]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Ahvaz","adm0name":"Iran","adm1name":"Khuzestan","iso_a2":"IR"},"coordinates":[635327,690046]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Basra","adm0name":"Iraq","adm1name":"Al-Basrah","iso_a2":"IQ"},"coordinates":[632809,685504]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Bandar-e-Abbas","adm0name":"Iran","adm1name":"Hormozgan","iso_a2":"IR"},"coordinates":[656311,665886]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Hamadan","adm0name":"Iran","adm1name":"Hamadan","iso_a2":"IR"},"coordinates":[634763,710864]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Tabriz","adm0name":"Iran","adm1name":"East Azarbaijan","iso_a2":"IR"},"coordinates":[628608,730369]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Ludhiana","adm0name":"India","adm1name":"Punjab","iso_a2":"IN"},"coordinates":[710750,687959]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Kota","adm0name":"India","adm1name":"Rajasthan","iso_a2":"IN"},"coordinates":[710647,653906]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Jodhpur","adm0name":"India","adm1name":"Rajasthan","iso_a2":"IN"},"coordinates":[702818,660493]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Shymkent","adm0name":"Kazakhstan","adm1name":"South Kazakhstan","iso_a2":"KZ"},"coordinates":[693319,755440]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Taraz","adm0name":"Kazakhstan","adm1name":"Zhambyl","iso_a2":"KZ"},"coordinates":[698236,758876]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Lucknow","adm0name":"India","adm1name":"Uttar Pradesh","iso_a2":"IN"},"coordinates":[724758,663830]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Saharanpur","adm0name":"India","adm1name":"Uttar Pradesh","iso_a2":"IN"},"coordinates":[715416,682273]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Ranchi","adm0name":"India","adm1name":"Jharkhand","iso_a2":"IN"},"coordinates":[737021,643183]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Bhagalpur","adm0name":"India","adm1name":"Bihar","iso_a2":"IN"},"coordinates":[741610,654191]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Raipur","adm0name":"India","adm1name":"Chhattisgarh","iso_a2":"IN"},"coordinates":[726757,630534]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Jabalpur","adm0name":"India","adm1name":"Madhya Pradesh","iso_a2":"IN"},"coordinates":[722091,642028]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Indore","adm0name":"India","adm1name":"Madhya Pradesh","iso_a2":"IN"},"coordinates":[710730,639303]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Pondicherry","adm0name":"India","adm1name":"Puducherry","iso_a2":"IN"},"coordinates":[721749,575425]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Salem","adm0name":"India","adm1name":"Tamil Nadu","iso_a2":"IN"},"coordinates":[717160,573867]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Tiruchirappalli","adm0name":"India","adm1name":"Tamil Nadu","iso_a2":"IN"},"coordinates":[718577,568772]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Pointe-Noire","adm0name":"Congo (Brazzaville)","adm1name":"Kouilou","iso_a2":"CG"},"coordinates":[533000,476457]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Kankan","adm0name":"Guinea","adm1name":"Kankan","iso_a2":"GN"},"coordinates":[474138,566272]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Nzerekore","adm0name":"Guinea","adm1name":"Nzerekore","iso_a2":"GN"},"coordinates":[475472,550691]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Bouake","adm0name":"Ivory Coast","adm1name":"Vallée du Bandama","iso_a2":"CI"},"coordinates":[486027,550276]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"St.-Denis","adm0name":"France","adm1name":"La Réunion","iso_a2":"RE"},"coordinates":[654022,381021]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Rio Branco","adm0name":"Brazil","adm1name":"Acre","iso_a2":"BR"},"coordinates":[311666,445671]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"São Luís","adm0name":"Brazil","adm1name":"Maranhão","iso_a2":"BR"},"coordinates":[377034,489822]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Porto Velho","adm0name":"Brazil","adm1name":"Rondônia","iso_a2":"BR"},"coordinates":[322500,452878]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Alvorada","adm0name":"Brazil","adm1name":"Tocantins","iso_a2":"BR"},"coordinates":[363661,430839]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Corumba","adm0name":"Brazil","adm1name":"Mato Grosso do Sul","iso_a2":"BR"},"coordinates":[339861,392057]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Belo Horizonte","adm0name":"Brazil","adm1name":"Minas Gerais","iso_a2":"BR"},"coordinates":[378008,386743]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Montes Claros","adm0name":"Brazil","adm1name":"Minas Gerais","iso_a2":"BR"},"coordinates":[378166,405660]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Uberlandia","adm0name":"Brazil","adm1name":"Minas Gerais","iso_a2":"BR"},"coordinates":[365888,392745]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Colider","adm0name":"Brazil","adm1name":"Mato Grosso","iso_a2":"BR"},"coordinates":[345970,440630]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Alta Floresta","adm0name":"Brazil","adm1name":"Mato Grosso","iso_a2":"BR"},"coordinates":[344694,446065]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Cuiaba","adm0name":"Brazil","adm1name":"Mato Grosso","iso_a2":"BR"},"coordinates":[344203,412487]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Pelotas","adm0name":"Brazil","adm1name":"Rio Grande do Sul","iso_a2":"BR"},"coordinates":[354639,316616]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Caxias do Sul","adm0name":"Brazil","adm1name":"Rio Grande do Sul","iso_a2":"BR"},"coordinates":[357860,331842]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Ponta Grossa","adm0name":"Brazil","adm1name":"Paraná","iso_a2":"BR"},"coordinates":[360666,356072]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Teresina","adm0name":"Brazil","adm1name":"Piauí","iso_a2":"BR"},"coordinates":[381161,474543]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Maceio","adm0name":"Brazil","adm1name":"Alagoas","iso_a2":"BR"},"coordinates":[400745,447735]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Vitoria da Conquista","adm0name":"Brazil","adm1name":"Bahia","iso_a2":"BR"},"coordinates":[386555,416739]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Barreiras","adm0name":"Brazil","adm1name":"Bahia","iso_a2":"BR"},"coordinates":[375000,432794]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Vila Velha","adm0name":"Brazil","adm1name":"Espírito Santo","iso_a2":"BR"},"coordinates":[388005,384050]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Natal","adm0name":"Brazil","adm1name":"Rio Grande do Norte","iso_a2":"BR"},"coordinates":[402105,470485]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Thompson","adm0name":"Canada","adm1name":"Manitoba","iso_a2":"CA"},"coordinates":[228148,835005]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Brandon","adm0name":"Canada","adm1name":"Manitoba","iso_a2":"CA"},"coordinates":[222361,799952]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Fort Smith","adm0name":"Canada","adm1name":"Alberta","iso_a2":"CA"},"coordinates":[189212,860185]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Fort McMurray","adm0name":"Canada","adm1name":"Alberta","iso_a2":"CA"},"coordinates":[190601,840831]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Peace River","adm0name":"Canada","adm1name":"Alberta","iso_a2":"CA"},"coordinates":[174213,837869]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Fort St. John","adm0name":"Canada","adm1name":"British Columbia","iso_a2":"CA"},"coordinates":[164352,837967]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Iqaluit","adm0name":"Canada","adm1name":"Nunavut","iso_a2":"CA"},"coordinates":[309722,882404]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Cambridge Bay","adm0name":"Canada","adm1name":"Nunavut","iso_a2":"CA"},"coordinates":[208240,914197]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Kugluktuk","adm0name":"Canada","adm1name":"Nunavut","iso_a2":"CA"},"coordinates":[180207,906387]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Chesterfield Inlet","adm0name":"Canada","adm1name":"Nunavut","iso_a2":"CA"},"coordinates":[248055,879962]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Arviat","adm0name":"Canada","adm1name":"Nunavut","iso_a2":"CA"},"coordinates":[238726,866752]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Taloyoak","adm0name":"Canada","adm1name":"Nunavut","iso_a2":"CA"},"coordinates":[240185,916664]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Igloolik","adm0name":"Canada","adm1name":"Nunavut","iso_a2":"CA"},"coordinates":[272796,915024]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Dawson City","adm0name":"Canada","adm1name":"Yukon","iso_a2":"CA"},"coordinates":[112731,884277]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Timmins","adm0name":"Canada","adm1name":"Ontario","iso_a2":"CA"},"coordinates":[274074,791855]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"North Bay","adm0name":"Canada","adm1name":"Ontario","iso_a2":"CA"},"coordinates":[279305,779019]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Kuujjuarapik","adm0name":"Canada","adm1name":"Québec","iso_a2":"CA"},"coordinates":[283983,832229]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Kuujjuaq","adm0name":"Canada","adm1name":"Québec","iso_a2":"CA"},"coordinates":[310000,848928]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Sydney","adm0name":"Canada","adm1name":"Nova Scotia","iso_a2":"CA"},"coordinates":[332833,777634]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Labrador City","adm0name":"Canada","adm1name":"Newfoundland and Labrador","iso_a2":"CA"},"coordinates":[314122,818366]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Ebolowa","adm0name":"Cameroon","adm1name":"Sud","iso_a2":"CM"},"coordinates":[530972,521898]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Bambari","adm0name":"Central African Republic","adm1name":"Ouaka","iso_a2":"CF"},"coordinates":[557408,538854]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Venice","adm0name":"Italy","adm1name":"Veneto","iso_a2":"IT"},"coordinates":[534263,773916]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"El Calafate","adm0name":"Argentina","adm1name":"Santa Cruz","iso_a2":"AR"},"coordinates":[299166,206520]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"San Juan","adm0name":"Argentina","adm1name":"San Juan","iso_a2":"AR"},"coordinates":[309667,317800]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Rawson","adm0name":"Argentina","adm1name":"Chubut","iso_a2":"AR"},"coordinates":[319167,248188]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Neuquen","adm0name":"Argentina","adm1name":"Neuquén","iso_a2":"AR"},"coordinates":[310944,273959]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Trinidad","adm0name":"Bolivia","adm1name":"El Beni","iso_a2":"BO"},"coordinates":[319722,416838]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Santa Rosa","adm0name":"Argentina","adm1name":"La Pampa","iso_a2":"AR"},"coordinates":[321389,287763]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"San Carlos de Bariloche","adm0name":"Argentina","adm1name":"Río Negro","iso_a2":"AR"},"coordinates":[301944,260926]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Salta","adm0name":"Argentina","adm1name":"Salta","iso_a2":"AR"},"coordinates":[318286,357889]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Tucumán","adm0name":"Argentina","adm1name":"Tucumán","iso_a2":"AR"},"coordinates":[318837,345858]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Formosa","adm0name":"Argentina","adm1name":"Formosa","iso_a2":"AR"},"coordinates":[338380,349657]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Santa Fe","adm0name":"Argentina","adm1name":"Santa Fe","iso_a2":"AR"},"coordinates":[331416,317363]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Rosario","adm0name":"Argentina","adm1name":"Santa Fe","iso_a2":"AR"},"coordinates":[331477,309511]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Campinas","adm0name":"Brazil","adm1name":"São Paulo","iso_a2":"BR"},"coordinates":[369161,369059]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Sorocaba","adm0name":"Brazil","adm1name":"São Paulo","iso_a2":"BR"},"coordinates":[368139,365552]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Ribeirao Preto","adm0name":"Brazil","adm1name":"São Paulo","iso_a2":"BR"},"coordinates":[367139,379296]},{"type":"Point","properties":{"scalerank":4,"labelrank":1,"name":"Petrolina","adm0name":"Brazil","adm1name":"Pernambuco","iso_a2":"BR"},"coordinates":[387472,449145]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Bamenda","adm0name":"Cameroon","adm1name":"Nord-Ouest","iso_a2":"CM"},"coordinates":[528194,540027]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Garoua","adm0name":"Cameroon","adm1name":"Nord","iso_a2":"CM"},"coordinates":[537194,559815]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Herat","adm0name":"Afghanistan","adm1name":"Hirat","iso_a2":"AF"},"coordinates":[672694,708103]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Mazar-e Sharif","adm0name":"Afghanistan","adm1name":"Balkh","iso_a2":"AF"},"coordinates":[686388,722144]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Battambang","adm0name":"Cambodia","adm1name":"Batdâmbâng","iso_a2":"KH"},"coordinates":[786666,582327]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Siem Reap","adm0name":"Cambodia","adm1name":"Siemréab","iso_a2":"KH"},"coordinates":[788471,583907]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Malanje","adm0name":"Angola","adm1name":"Malanje","iso_a2":"AO"},"coordinates":[545389,448198]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Benguela","adm0name":"Angola","adm1name":"Benguela","iso_a2":"AO"},"coordinates":[537242,430198]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Lubango","adm0name":"Angola","adm1name":"Huíla","iso_a2":"AO"},"coordinates":[537471,416383]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Namibe","adm0name":"Angola","adm1name":"Namibe","iso_a2":"AO"},"coordinates":[533778,414725]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Tarija","adm0name":"Bolivia","adm1name":"Tarija","iso_a2":"BO"},"coordinates":[320138,377243]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Bridgetown","adm0name":"Barbados","adm1name":"Saint Michael","iso_a2":"BB"},"coordinates":[334399,582339]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Annaba","adm0name":"Algeria","adm1name":"Annaba","iso_a2":"DZ"},"coordinates":[521555,723448]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Parakou","adm0name":"Benin","adm1name":"Borgou","iso_a2":"BJ"},"coordinates":[507278,560052]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Porto-Novo","adm0name":"Benin","adm1name":"Ouémé","iso_a2":"BJ"},"coordinates":[507268,543127]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Constantine","adm0name":"Algeria","adm1name":"Constantine","iso_a2":"DZ"},"coordinates":[518332,720130]},{"type":"Point","properties":{"scalerank":4,"labelrank":6,"name":"Brest","adm0name":"Belarus","adm1name":"Brest","iso_a2":"BY"},"coordinates":[565833,813381]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Khulna","adm0name":"Bangladesh","adm1name":"Khulna","iso_a2":"BD"},"coordinates":[748772,640043]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Francistown","adm0name":"Botswana","adm1name":"Central","iso_a2":"BW"},"coordinates":[576388,379296]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Mahalapye","adm0name":"Botswana","adm1name":"Central","iso_a2":"BW"},"coordinates":[574500,367862]},{"type":"Point","properties":{"scalerank":4,"labelrank":7,"name":"Serowe","adm0name":"Botswana","adm1name":"Central","iso_a2":"BW"},"coordinates":[574194,372068]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Katherine","adm0name":"Australia","adm1name":"Northern Territory","iso_a2":"AU"},"coordinates":[867406,419010]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Busselton","adm0name":"Australia","adm1name":"Western Australia","iso_a2":"AU"},"coordinates":[820412,305321]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Mandurah","adm0name":"Australia","adm1name":"Western Australia","iso_a2":"AU"},"coordinates":[821519,312034]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Broome","adm0name":"Australia","adm1name":"Western Australia","iso_a2":"AU"},"coordinates":[839530,398304]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Kalgoorlie","adm0name":"Australia","adm1name":"Western Australia","iso_a2":"AU"},"coordinates":[837388,322627]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Albany","adm0name":"Australia","adm1name":"Western Australia","iso_a2":"AU"},"coordinates":[827476,297261]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Port Hedland","adm0name":"Australia","adm1name":"Western Australia","iso_a2":"AU"},"coordinates":[829460,384389]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Karratha","adm0name":"Australia","adm1name":"Western Australia","iso_a2":"AU"},"coordinates":[824638,381901]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Geraldton","adm0name":"Australia","adm1name":"Western Australia","iso_a2":"AU"},"coordinates":[818332,334291]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Griffith","adm0name":"Australia","adm1name":"New South Wales","iso_a2":"AU"},"coordinates":[905665,301567]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Orange","adm0name":"Australia","adm1name":"New South Wales","iso_a2":"AU"},"coordinates":[914166,307551]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Dubbo","adm0name":"Australia","adm1name":"New South Wales","iso_a2":"AU"},"coordinates":[912769,313595]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Armidale","adm0name":"Australia","adm1name":"New South Wales","iso_a2":"AU"},"coordinates":[921297,323948]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Broken Hill","adm0name":"Australia","adm1name":"New South Wales","iso_a2":"AU"},"coordinates":[892869,315431]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Port Lincoln","adm0name":"Australia","adm1name":"South Australia","iso_a2":"AU"},"coordinates":[877407,298942]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Whyalla","adm0name":"Australia","adm1name":"South Australia","iso_a2":"AU"},"coordinates":[882114,309062]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Portland","adm0name":"Australia","adm1name":"Victoria","iso_a2":"AU"},"coordinates":[893305,277573]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Bendigo","adm0name":"Australia","adm1name":"Victoria","iso_a2":"AU"},"coordinates":[900777,286934]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Wangaratta","adm0name":"Australia","adm1name":"Victoria","iso_a2":"AU"},"coordinates":[906388,289304]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Windorah","adm0name":"Australia","adm1name":"Queensland","iso_a2":"AU"},"coordinates":[896250,354039]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Mount Isa","adm0name":"Australia","adm1name":"Queensland","iso_a2":"AU"},"coordinates":[887471,381939]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Rockhampton","adm0name":"Australia","adm1name":"Queensland","iso_a2":"AU"},"coordinates":[918110,366299]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Cairns","adm0name":"Australia","adm1name":"Queensland","iso_a2":"AU"},"coordinates":[904897,404666]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Gold Coast","adm0name":"Australia","adm1name":"Queensland","iso_a2":"AU"},"coordinates":[926245,338350]},{"type":"Point","properties":{"scalerank":4,"labelrank":3,"name":"Devonport","adm0name":"Australia","adm1name":"Tasmania","iso_a2":"AU"},"coordinates":[906475,260673]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Bobo Dioulasso","adm0name":"Burkina Faso","adm1name":"Houet","iso_a2":"BF"},"coordinates":[488083,570952]},{"type":"Point","properties":{"scalerank":4,"labelrank":2,"name":"Rajshahi","adm0name":"Bangladesh","adm1name":"Rajshahi","iso_a2":"BD"},"coordinates":[746118,649137]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Mandalay","adm0name":"Myanmar","adm1name":"Mandalay","iso_a2":"MM"},"coordinates":[766897,634889]},{"type":"Point","properties":{"scalerank":4,"labelrank":5,"name":"Sittwe","adm0name":"Myanmar","adm1name":"Rakhine","iso_a2":"MM"},"coordinates":[758000,624035]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Bujumbura","adm0name":"Burundi","adm1name":"Bujumbura Mairie","iso_a2":"BI"},"coordinates":[581555,484715]},{"type":"Point","properties":{"scalerank":4,"labelrank":0,"name":"Pago Pago","adm0name":"American Samoa","iso_a2":"AS"},"coordinates":[25815,420136]},{"type":"Point","properties":{"scalerank":4,"labelrank":0,"name":"Kingstown","adm0name":"Saint Vincent and the Grenadines","iso_a2":"VC"},"coordinates":[329966,582614]},{"type":"Point","properties":{"scalerank":4,"labelrank":0,"name":"Castries","adm0name":"Saint Lucia","iso_a2":"LC"},"coordinates":[330555,587671]},{"type":"Point","properties":{"scalerank":4,"labelrank":0,"name":"Basseterre","adm0name":"Saint Kitts and Nevis","iso_a2":"KN"},"coordinates":[325786,607222]},{"type":"Point","properties":{"scalerank":4,"labelrank":0,"name":"Las Palmas","adm0name":"Spain","iso_a2":"ES"},"coordinates":[457138,671194]},{"type":"Point","properties":{"scalerank":4,"labelrank":0,"name":"Berbera","adm0name":"Somaliland","iso_a2":"-99"},"coordinates":[625045,566542]},{"type":"Point","properties":{"scalerank":4,"labelrank":0,"name":"Port Louis","adm0name":"Mauritius","iso_a2":"MU"},"coordinates":[659722,385241]},{"type":"Point","properties":{"scalerank":4,"labelrank":0,"name":"Gaza","adm0name":"Palestine","iso_a2":"PS"},"coordinates":[595680,691515]},{"type":"Point","properties":{"scalerank":4,"labelrank":0,"name":"Saint George's","adm0name":"Grenada","iso_a2":"GD"},"coordinates":[328495,576122]},{"type":"Point","properties":{"scalerank":4,"labelrank":0,"name":"Papeete","adm0name":"French Polynesia","iso_a2":"PF"},"coordinates":[84537,400842]},{"type":"Point","properties":{"scalerank":4,"labelrank":0,"name":"Manama","adm0name":"Bahrain","iso_a2":"BH"},"coordinates":[640508,660152]},{"type":"Point","properties":{"scalerank":4,"labelrank":0,"name":"Freeport","adm0name":"The Bahamas","iso_a2":"BS"},"coordinates":[281389,661912]},{"type":"Point","properties":{"scalerank":4,"labelrank":0,"name":"Saint John's","adm0name":"Antigua and Barbuda","iso_a2":"AG"},"coordinates":[328194,606132]},{"type":"Point","properties":{"scalerank":4,"labelrank":8,"name":"Taichung","adm0name":"Taiwan","adm1name":"Taichung City","iso_a2":"TW"},"coordinates":[835226,647805]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Kozhikode","adm0name":"India","adm1name":"Kerala","iso_a2":"IN"},"coordinates":[710466,571381]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Bhubaneshwar","adm0name":"India","adm1name":"Orissa","iso_a2":"IN"},"coordinates":[738403,624820]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Jamshedpur","adm0name":"India","adm1name":"Jharkhand","iso_a2":"IN"},"coordinates":[739431,639733]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Montevideo","adm0name":"Uruguay","adm1name":"Montevideo","iso_a2":"UY"},"coordinates":[343963,298213]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Helena","adm0name":"United States of America","adm1name":"Montana","iso_a2":"US"},"coordinates":[188791,780754]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Bismarck","adm0name":"United States of America","adm1name":"North Dakota","iso_a2":"US"},"coordinates":[220046,782031]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Boise","adm0name":"United States of America","adm1name":"Idaho","iso_a2":"US"},"coordinates":[177146,763074]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"San Jose","adm0name":"United States of America","adm1name":"California","iso_a2":"US"},"coordinates":[161523,725711]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Sacramento","adm0name":"United States of America","adm1name":"California","iso_a2":"US"},"coordinates":[162578,733265]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Las Vegas","adm0name":"United States of America","adm1name":"Nevada","iso_a2":"US"},"coordinates":[179939,719253]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Santa Fe","adm0name":"United States of America","adm1name":"New Mexico","iso_a2":"US"},"coordinates":[205729,716142]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Portland","adm0name":"United States of America","adm1name":"Oregon","iso_a2":"US"},"coordinates":[159217,774410]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Salt Lake City","adm0name":"United States of America","adm1name":"Utah","iso_a2":"US"},"coordinates":[189078,746298]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Cheyenne","adm0name":"United States of America","adm1name":"Wyoming","iso_a2":"US"},"coordinates":[208834,748449]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Des Moines","adm0name":"United States of America","adm1name":"Iowa","iso_a2":"US"},"coordinates":[239944,751055]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Omaha","adm0name":"United States of America","adm1name":"Nebraska","iso_a2":"US"},"coordinates":[233305,749041]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Oklahoma City","adm0name":"United States of America","adm1name":"Oklahoma","iso_a2":"US"},"coordinates":[229109,714869]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Pierre","adm0name":"United States of America","adm1name":"South Dakota","iso_a2":"US"},"coordinates":[221248,767575]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"San Antonio","adm0name":"United States of America","adm1name":"Texas","iso_a2":"US"},"coordinates":[226363,679425]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"San Cristobal","adm0name":"Venezuela","adm1name":"Táchira","iso_a2":"VE"},"coordinates":[299305,550750]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Valencia","adm0name":"Venezuela","adm1name":"Carabobo","iso_a2":"VE"},"coordinates":[311161,565336]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Jackson","adm0name":"United States of America","adm1name":"Mississippi","iso_a2":"US"},"coordinates":[249486,696070]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Raleigh","adm0name":"United States of America","adm1name":"North Carolina","iso_a2":"US"},"coordinates":[281542,716923]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Cleveland","adm0name":"United States of America","adm1name":"Ohio","iso_a2":"US"},"coordinates":[273064,750415]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Cincinnati","adm0name":"United States of America","adm1name":"Ohio","iso_a2":"US"},"coordinates":[265392,736741]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Nashville","adm0name":"United States of America","adm1name":"Tennessee","iso_a2":"US"},"coordinates":[258939,719016]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Memphis","adm0name":"United States of America","adm1name":"Tennessee","iso_a2":"US"},"coordinates":[249994,712796]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Norfolk","adm0name":"United States of America","adm1name":"Virginia","iso_a2":"US"},"coordinates":[288111,723033]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Milwaukee","adm0name":"United States of America","adm1name":"Wisconsin","iso_a2":"US"},"coordinates":[255773,759792]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Buffalo","adm0name":"United States of America","adm1name":"New York","iso_a2":"US"},"coordinates":[280884,758769]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Pittsburgh","adm0name":"United States of America","adm1name":"Pennsylvania","iso_a2":"US"},"coordinates":[277772,744254]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Ciudad Guayana","adm0name":"Venezuela","adm1name":"Bolívar","iso_a2":"VE"},"coordinates":[326055,554305]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Lome","adm0name":"Togo","adm1name":"Maritime","iso_a2":"TG"},"coordinates":[503390,541057]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Tunis","adm0name":"Tunisia","adm1name":"Tunis","iso_a2":"TN"},"coordinates":[528276,722753]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Kodiak","adm0name":"United States of America","adm1name":"Alaska","iso_a2":"US"},"coordinates":[76648,847091]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Cold Bay","adm0name":"United States of America","adm1name":"Alaska","iso_a2":"US"},"coordinates":[48014,831747]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Bethel","adm0name":"United States of America","adm1name":"Alaska","iso_a2":"US"},"coordinates":[50678,864884]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Point Hope","adm0name":"United States of America","adm1name":"Alaska","iso_a2":"US"},"coordinates":[36644,909640]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Barrow","adm0name":"United States of America","adm1name":"Alaska","iso_a2":"US"},"coordinates":[64476,927075]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Nome","adm0name":"United States of America","adm1name":"Alaska","iso_a2":"US"},"coordinates":[40538,886881]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Valdez","adm0name":"United States of America","adm1name":"Alaska","iso_a2":"US"},"coordinates":[93477,866914]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Juneau","adm0name":"United States of America","adm1name":"Alaska","iso_a2":"US"},"coordinates":[126611,850197]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Fairbanks","adm0name":"United States of America","adm1name":"Alaska","iso_a2":"US"},"coordinates":[89693,888841]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Prudhoe Bay","adm0name":"United States of America","adm1name":"Alaska","iso_a2":"US"},"coordinates":[87030,921160]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Sevastapol","adm0name":"Ukraine","adm1name":"Crimea","iso_a2":"UA"},"coordinates":[592958,768948]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Abu Dhabi","adm0name":"United Arab Emirates","adm1name":"Abu Dhabi","iso_a2":"AE"},"coordinates":[651018,649669]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Ashgabat","adm0name":"Turkmenistan","adm1name":"Ahal","iso_a2":"TM"},"coordinates":[662175,729550]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Samarqand","adm0name":"Uzbekistan","adm1name":"Samarkand","iso_a2":"UZ"},"coordinates":[685958,739740]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Lusaka","adm0name":"Zambia","adm1name":"Lusaka","iso_a2":"ZM"},"coordinates":[578559,413393]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Harare","adm0name":"Zimbabwe","adm1name":"Harare","iso_a2":"ZW"},"coordinates":[586230,399168]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Bulawayo","adm0name":"Zimbabwe","adm1name":"Bulawayo","iso_a2":"ZW"},"coordinates":[579388,385221]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Dili","adm0name":"East Timor","adm1name":"Dili","iso_a2":"TL"},"coordinates":[848831,454008]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Port Vila","adm0name":"Vanuatu","adm1name":"Shefa","iso_a2":"VU"},"coordinates":[967545,399657]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Tegucigalpa","adm0name":"Honduras","adm1name":"Francisco Morazán","iso_a2":"HN"},"coordinates":[257724,588276]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Georgetown","adm0name":"Guyana","adm1name":"East Berbice-Corentyne","iso_a2":"GY"},"coordinates":[338424,545015]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Reykjavík","adm0name":"Iceland","adm1name":"Suðurnes","iso_a2":"IS"},"coordinates":[439028,884771]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Port-au-Prince","adm0name":"Haiti","adm1name":"Ouest","iso_a2":"HT"},"coordinates":[299061,614574]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Glasgow","adm0name":"United Kingdom","adm1name":"Glasgow","iso_a2":"GB"},"coordinates":[488187,835753]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Kampala","adm0name":"Uganda","adm1name":"Kampala","iso_a2":"UG"},"coordinates":[590503,506605]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Aden","adm0name":"Yemen","adm1name":"`Adan","iso_a2":"YE"},"coordinates":[625025,580430]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Paramaribo","adm0name":"Suriname","adm1name":"Paramaribo","iso_a2":"SR"},"coordinates":[346758,539286]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Seville","adm0name":"Spain","adm1name":"Andalucía","iso_a2":"ES"},"coordinates":[483389,726322]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Zinder","adm0name":"Niger","adm1name":"Zinder","iso_a2":"NE"},"coordinates":[524953,586474]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Niamey","adm0name":"Niger","adm1name":"Niamey","iso_a2":"NE"},"coordinates":[505874,584808]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Port Sudan","adm0name":"Sudan","adm1name":"Red Sea","iso_a2":"SD"},"coordinates":[603378,620930]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Dushanbe","adm0name":"Tajikistan","adm1name":"Tadzhikistan Territories","iso_a2":"TJ"},"coordinates":[691038,733164]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Cusco","adm0name":"Peru","adm1name":"Cusco","iso_a2":"PE"},"coordinates":[300077,424589]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Tacna","adm0name":"Peru","adm1name":"Tacna","iso_a2":"PE"},"coordinates":[304861,398077]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Trujillo","adm0name":"Peru","adm1name":"La Libertad","iso_a2":"PE"},"coordinates":[280499,456610]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Ica","adm0name":"Peru","adm1name":"Ica","iso_a2":"PE"},"coordinates":[289651,421372]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Asuncion","adm0name":"Paraguay","adm1name":"Asunción","iso_a2":"PY"},"coordinates":[339879,354861]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Managua","adm0name":"Nicaragua","adm1name":"Managua","iso_a2":"NI"},"coordinates":[260359,576729]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Freetown","adm0name":"Sierra Leone","adm1name":"Western","iso_a2":"SL"},"coordinates":[463233,554909]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Agadez","adm0name":"Niger","adm1name":"Agadez","iso_a2":"NE"},"coordinates":[522174,605409]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Niyala","adm0name":"Sudan","adm1name":"South Darfur","iso_a2":"SD"},"coordinates":[569138,576166]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Wau","adm0name":"South Sudan","adm1name":"West Bahr-al-Ghazal","iso_a2":"SS"},"coordinates":[577750,550335]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Dongola","adm0name":"Sudan","adm1name":"Northern","iso_a2":"SD"},"coordinates":[584676,618269]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Kassala","adm0name":"Sudan","adm1name":"Kassala","iso_a2":"SD"},"coordinates":[601083,596309]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Tromsø","adm0name":"Norway","adm1name":"Troms","iso_a2":"NO"},"coordinates":[552755,917266]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Trondheim","adm0name":"Norway","adm1name":"Sør-Trøndelag","iso_a2":"NO"},"coordinates":[528934,880426]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Bergen","adm0name":"Norway","adm1name":"Hordaland","iso_a2":"NO"},"coordinates":[514790,862501]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Islamabad","adm0name":"Pakistan","adm1name":"F.C.T.","iso_a2":"PK"},"coordinates":[703235,704383]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Multan","adm0name":"Pakistan","adm1name":"Punjab","iso_a2":"PK"},"coordinates":[698480,683647]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Hyderabad","adm0name":"Pakistan","adm1name":"Sind","iso_a2":"PK"},"coordinates":[689924,655091]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Peshawar","adm0name":"Pakistan","adm1name":"N.W.F.P.","iso_a2":"PK"},"coordinates":[698702,706190]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Kathmandu","adm0name":"Nepal","adm1name":"Bhaktapur","iso_a2":"NP"},"coordinates":[736984,668935]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Nacala","adm0name":"Mozambique","adm1name":"Nampula","iso_a2":"MZ"},"coordinates":[613096,418702]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Bloemfontein","adm0name":"South Africa","adm1name":"Orange Free State","iso_a2":"ZA"},"coordinates":[572860,332197]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Pretoria","adm0name":"South Africa","adm1name":"Gauteng","iso_a2":"ZA"},"coordinates":[578409,352429]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Port Moresby","adm0name":"Papua New Guinea","adm1name":"Central","iso_a2":"PG"},"coordinates":[908867,448644]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Honiara","adm0name":"Solomon Islands","adm1name":"Guadalcanal","iso_a2":"SB"},"coordinates":[944304,448802]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Panama City","adm0name":"Panama","adm1name":"Panama","iso_a2":"PA"},"coordinates":[279069,557859]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Fez","adm0name":"Morocco","adm1name":"Fès - Boulemane","iso_a2":"MA"},"coordinates":[486104,706483]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Rabat","adm0name":"Morocco","adm1name":"Rabat - Salé - Zemmour - Zaer","iso_a2":"MA"},"coordinates":[481009,706298]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Marrakesh","adm0name":"Morocco","adm1name":"Marrakech - Tensift - Al Haouz","iso_a2":"MA"},"coordinates":[477772,692119]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Chisinau","adm0name":"Moldova","adm1name":"Chisinau","iso_a2":"MD"},"coordinates":[580159,783196]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Beira","adm0name":"Mozambique","adm1name":"Sofala","iso_a2":"MZ"},"coordinates":[596860,387294]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Port Elizabeth","adm0name":"South Africa","adm1name":"Eastern Cape","iso_a2":"ZA"},"coordinates":[571105,303474]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Maputo","adm0name":"Mozambique","adm1name":"Maputo","iso_a2":"MZ"},"coordinates":[590520,350958]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Tomsk","adm0name":"Russia","adm1name":"Tomsk","iso_a2":"RU"},"coordinates":[736041,839419]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Anadyr","adm0name":"Russia","adm1name":"Chukchi Autonomous Okrug","iso_a2":"RU"},"coordinates":[992986,888248]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Murmansk","adm0name":"Russia","adm1name":"Murmansk","iso_a2":"RU"},"coordinates":[591944,913327]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Archangel","adm0name":"Russia","adm1name":"Arkhangel'sk","iso_a2":"RU"},"coordinates":[612625,887289]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Nizhny Novgorod","adm0name":"Russia","adm1name":"Nizhegorod","iso_a2":"RU"},"coordinates":[622217,838471]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Volgograd","adm0name":"Russia","adm1name":"Volgograd","iso_a2":"RU"},"coordinates":[623605,793308]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Ufa","adm0name":"Russia","adm1name":"Bashkortostan","iso_a2":"RU"},"coordinates":[655660,829329]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Yekaterinburg","adm0name":"Russia","adm1name":"Sverdlovsk","iso_a2":"RU"},"coordinates":[668327,841534]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Samara","adm0name":"Russia","adm1name":"Samara","iso_a2":"RU"},"coordinates":[639303,819880]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Kazan","adm0name":"Russia","adm1name":"Tatarstan","iso_a2":"RU"},"coordinates":[636456,835017]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Surgut","adm0name":"Russia","adm1name":"Khanty-Mansiy","iso_a2":"RU"},"coordinates":[703958,867649]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Barnaul","adm0name":"Russia","adm1name":"Altay","iso_a2":"RU"},"coordinates":[732624,820816]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Novosibirsk","adm0name":"Russia","adm1name":"Novosibirsk","iso_a2":"RU"},"coordinates":[730438,830751]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Mogadishu","adm0name":"Somalia","adm1name":"Banaadir","iso_a2":"SO"},"coordinates":[626013,516973]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Muscat","adm0name":"Oman","adm1name":"Muscat","iso_a2":"OM"},"coordinates":[662758,644613]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Colombo","adm0name":"Sri Lanka","adm1name":"Colombo","iso_a2":"LK"},"coordinates":[721827,545785]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Cebu","adm0name":"Philippines","adm1name":"Cebu","iso_a2":"PH"},"coordinates":[844161,565868]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Iloilo","adm0name":"Philippines","adm1name":"Iloilo","iso_a2":"PH"},"coordinates":[840402,568139]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Davao","adm0name":"Philippines","adm1name":"Davao Del Sur","iso_a2":"PH"},"coordinates":[848966,546852]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Bratsk","adm0name":"Russia","adm1name":"Irkutsk","iso_a2":"RU"},"coordinates":[782263,837417]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Irkutsk","adm0name":"Russia","adm1name":"Irkutsk","iso_a2":"RU"},"coordinates":[789569,814685]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Krasnoyarsk","adm0name":"Russia","adm1name":"Krasnoyarsk","iso_a2":"RU"},"coordinates":[757955,836581]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Dickson","adm0name":"Russia","adm1name":"Taymyr","iso_a2":"RU"},"coordinates":[723736,940205]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Chita","adm0name":"Russia","adm1name":"Chita","iso_a2":"RU"},"coordinates":[815180,813115]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Vladivostok","adm0name":"Russia","adm1name":"Primor'ye","iso_a2":"RU"},"coordinates":[866416,760239]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Nizhneyansk","adm0name":"Russia","adm1name":"Sakha (Yakutia)","iso_a2":"RU"},"coordinates":[877962,927920]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Yakutsk","adm0name":"Russia","adm1name":"Sakha (Yakutia)","iso_a2":"RU"},"coordinates":[860374,872240]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Tiksi","adm0name":"Russia","adm1name":"Sakha (Yakutia)","iso_a2":"RU"},"coordinates":[857874,929067]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Magadan","adm0name":"Russia","adm1name":"Maga Buryatdan","iso_a2":"RU"},"coordinates":[918916,857666]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Tijuana","adm0name":"Mexico","adm1name":"Baja California","iso_a2":"MX"},"coordinates":[174772,697273]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Chihuahua","adm0name":"Mexico","adm1name":"Chihuahua","iso_a2":"MX"},"coordinates":[205314,674434]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Mazatlan","adm0name":"Mexico","adm1name":"Sinaloa","iso_a2":"MX"},"coordinates":[204388,642289]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Tampico","adm0name":"Mexico","adm1name":"Tamaulipas","iso_a2":"MX"},"coordinates":[228139,636832]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Acapulco","adm0name":"Mexico","adm1name":"Guerrero","iso_a2":"MX"},"coordinates":[222456,604544]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Veracruz","adm0name":"Mexico","adm1name":"Veracruz","iso_a2":"MX"},"coordinates":[232889,618332]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Tuxtla Gutierrez","adm0name":"Mexico","adm1name":"Chiapas","iso_a2":"MX"},"coordinates":[241250,603952]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Cancun","adm0name":"Mexico","adm1name":"Quintana Roo","iso_a2":"MX"},"coordinates":[258805,630138]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Merida","adm0name":"Mexico","adm1name":"Yucatán","iso_a2":"MX"},"coordinates":[251059,628944]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Enugu","adm0name":"Nigeria","adm1name":"Enugu","iso_a2":"NG"},"coordinates":[520833,542930]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Sokoto","adm0name":"Nigeria","adm1name":"Sokoto","iso_a2":"NG"},"coordinates":[514555,582090]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Perm","adm0name":"Russia","adm1name":"Perm'","iso_a2":"RU"},"coordinates":[656244,848347]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Erdenet","adm0name":"Mongolia","adm1name":"Orhon","iso_a2":"MN"},"coordinates":[789217,795331]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Ulaanbaatar","adm0name":"Mongolia","adm1name":"Ulaanbaatar","iso_a2":"MN"},"coordinates":[796984,788609]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Wellington","adm0name":"New Zealand","adm1name":"Manawatu-Wanganui","iso_a2":"NZ"},"coordinates":[985509,260037]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Mbeya","adm0name":"Tanzania","adm1name":"Mbeya","iso_a2":"TZ"},"coordinates":[592861,452049]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Windhoek","adm0name":"Namibia","adm1name":"Khomas","iso_a2":"NA"},"coordinates":[547454,371002]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Grootfontein","adm0name":"Namibia","adm1name":"Otjozondjupa","iso_a2":"NA"},"coordinates":[550323,388796]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Zanzibar","adm0name":"Tanzania","adm1name":"Zanzibar West","iso_a2":"TZ"},"coordinates":[608889,468223]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Christchurch","adm0name":"New Zealand","adm1name":"Canterbury","iso_a2":"NZ"},"coordinates":[979527,246796]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Valencia","adm0name":"Spain","adm1name":"Comunidad Valenciana","iso_a2":"ES"},"coordinates":[498883,738656]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Palana","adm0name":"Russia","adm1name":"Kamchatka","iso_a2":"RU"},"coordinates":[944305,854757]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Petropavlovsk Kamchatskiy","adm0name":"Russia","adm1name":"Kamchatka","iso_a2":"RU"},"coordinates":[940619,819080]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Abuja","adm0name":"Nigeria","adm1name":"Federal Capital Territory","iso_a2":"NG"},"coordinates":[520920,558542]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Padang","adm0name":"Indonesia","adm1name":"Sumatera Barat","iso_a2":"ID"},"coordinates":[778771,499041]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Bissau","adm0name":"Guinea Bissau","adm1name":"Bissau","iso_a2":"GW"},"coordinates":[456670,575011]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Palermo","adm0name":"Italy","adm1name":"Sicily","iso_a2":"IT"},"coordinates":[537078,730599]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Amman","adm0name":"Jordan","adm1name":"Amman","iso_a2":"JO"},"coordinates":[599808,694015]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Vilnius","adm0name":"Lithuania","adm1name":"Vilniaus","iso_a2":"LT"},"coordinates":[570324,828686]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Riga","adm0name":"Latvia","adm1name":"Riga","iso_a2":"LV"},"coordinates":[566943,842115]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Bishkek","adm0name":"Kyrgyzstan","adm1name":"Bishkek","iso_a2":"KG"},"coordinates":[707175,758728]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Jiayuguan","adm0name":"China","adm1name":"Gansu","iso_a2":"CN"},"coordinates":[773055,740629]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Xining","adm0name":"China","adm1name":"Gansu","iso_a2":"CN"},"coordinates":[782688,721682]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Guilin","adm0name":"China","adm1name":"Guangxi","iso_a2":"CN"},"coordinates":[806327,654499]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Huainan","adm0name":"China","adm1name":"Anhui","iso_a2":"CN"},"coordinates":[824938,698043]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Shantou","adm0name":"China","adm1name":"Guangdong","iso_a2":"CN"},"coordinates":[824077,643183]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Tarakan","adm0name":"Indonesia","adm1name":"Kalimantan Timur","iso_a2":"ID"},"coordinates":[826757,524268]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Mombasa","adm0name":"Kenya","adm1name":"Coast","iso_a2":"KE"},"coordinates":[610243,480793]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Maseru","adm0name":"Lesotho","adm1name":"Maseru","iso_a2":"LS"},"coordinates":[576342,331032]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Antananarivo","adm0name":"Madagascar","adm1name":"Antananarivo","iso_a2":"MG"},"coordinates":[631985,392658]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Semarang","adm0name":"Indonesia","adm1name":"Jawa Tengah","iso_a2":"ID"},"coordinates":[806717,463455]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Palembang","adm0name":"Indonesia","adm1name":"Sumatera Selatan","iso_a2":"ID"},"coordinates":[790966,487074]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Bandjarmasin","adm0name":"Indonesia","adm1name":"Kalimantan Selatan","iso_a2":"ID"},"coordinates":[818277,484989]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Ujungpandang","adm0name":"Indonesia","adm1name":"Sulawesi Selatan","iso_a2":"ID"},"coordinates":[831749,474277]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Lyon","adm0name":"France","adm1name":"Rhône-Alpes","iso_a2":"FR"},"coordinates":[513411,775891]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Quito","adm0name":"Ecuador","adm1name":"Pichincha","iso_a2":"EC"},"coordinates":[281939,503455]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"San Jose","adm0name":"Costa Rica","adm1name":"San José","iso_a2":"CR"},"coordinates":[266428,563588]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"San Salvador","adm0name":"El Salvador","adm1name":"San Salvador","iso_a2":"SV"},"coordinates":[252208,585953]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Kingston","adm0name":"Jamaica","adm1name":"Kingston","iso_a2":"JM"},"coordinates":[286756,611221]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Cartagena","adm0name":"Colombia","adm1name":"Bolívar","iso_a2":"CO"},"coordinates":[290232,566341]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Mitu","adm0name":"Colombia","adm1name":"Vaupés","iso_a2":"CO"},"coordinates":[305073,511816]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Bumba","adm0name":"Congo (Kinshasa)","adm1name":"Équateur","iso_a2":"CD"},"coordinates":[562388,517692]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Ndjamena","adm0name":"Chad","adm1name":"Hadjer-Lamis","iso_a2":"TD"},"coordinates":[541797,576492]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Abeche","adm0name":"Chad","adm1name":"Ouaddaï","iso_a2":"TD"},"coordinates":[557860,586711]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Malabo","adm0name":"Equatorial Guinea","adm1name":"Bioko Norte","iso_a2":"GQ"},"coordinates":[524398,526934]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Luxor","adm0name":"Egypt","adm1name":"Qina","iso_a2":"EG"},"coordinates":[590694,656975]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Asmara","adm0name":"Eritrea","adm1name":"Anseba","iso_a2":"ER"},"coordinates":[608148,595558]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Zagreb","adm0name":"Croatia","adm1name":"Grad Zagreb","iso_a2":"HR"},"coordinates":[544444,776057]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Tallinn","adm0name":"Estonia","adm1name":"Harju","iso_a2":"EE"},"coordinates":[568688,856830]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Lhasa","adm0name":"China","adm1name":"Xizang","iso_a2":"CN"},"coordinates":[753055,680348]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Hami","adm0name":"China","adm1name":"Xinjiang Uygur","iso_a2":"CN"},"coordinates":[759763,758443]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Hotan","adm0name":"China","adm1name":"Xinjiang Uygur","iso_a2":"CN"},"coordinates":[722018,724513]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Kashgar","adm0name":"China","adm1name":"Xinjiang Uygur","iso_a2":"CN"},"coordinates":[711027,738593]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Yinchuan","adm0name":"China","adm1name":"Ningxia Hui","iso_a2":"CN"},"coordinates":[795197,732631]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Pingxiang","adm0name":"China","adm1name":"Jiangxi","iso_a2":"CN"},"coordinates":[816244,668362]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Nagasaki","adm0name":"Japan","adm1name":"Nagasaki","iso_a2":"JP"},"coordinates":[860790,698831]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Qiqihar","adm0name":"China","adm1name":"Heilongjiang","iso_a2":"CN"},"coordinates":[844410,785222]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Kikwit","adm0name":"Congo (Kinshasa)","adm1name":"Bandundu","iso_a2":"CD"},"coordinates":[552361,474917]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Matadi","adm0name":"Congo (Kinshasa)","adm1name":"Bas-Congo","iso_a2":"CD"},"coordinates":[537360,470257]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Kolwezi","adm0name":"Congo (Kinshasa)","adm1name":"Katanga","iso_a2":"CD"},"coordinates":[570756,441226]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Lubumbashi","adm0name":"Congo (Kinshasa)","adm1name":"Katanga","iso_a2":"CD"},"coordinates":[576327,435531]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Lilongwe","adm0name":"Malawi","adm1name":"Lilongwe","iso_a2":"MW"},"coordinates":[593842,421873]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Guatemala","adm0name":"Guatemala","adm1name":"Guatemala","iso_a2":"GT"},"coordinates":[248530,591351]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Cayenne","adm0name":"France","adm1name":"Guinaa","iso_a2":"GF"},"coordinates":[354639,533942]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Libreville","adm0name":"Gabon","adm1name":"Estuaire","iso_a2":"GA"},"coordinates":[526271,507000]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Vishakhapatnam","adm0name":"India","adm1name":"Andhra Pradesh","iso_a2":"IN"},"coordinates":[731396,609769]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Suva","adm0name":"Fiji","adm1name":"Central","iso_a2":"FJ"},"coordinates":[995670,397289]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Port-Gentil","adm0name":"Gabon","adm1name":"Ogooué-Maritime","iso_a2":"GA"},"coordinates":[524389,500451]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Timbuktu","adm0name":"Mali","adm1name":"Timbuktu","iso_a2":"ML"},"coordinates":[491620,604050]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Punta Arenas","adm0name":"Chile","adm1name":"Magallanes y Antártica Chilena","iso_a2":"CL"},"coordinates":[302944,189744]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Iquique","adm0name":"Chile","adm1name":"Tarapacá","iso_a2":"CL"},"coordinates":[305194,384747]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Antofagasta","adm0name":"Chile","adm1name":"Antofagasta","iso_a2":"CL"},"coordinates":[304444,364604]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Valparaiso","adm0name":"Chile","adm1name":"Valparaíso","iso_a2":"CL"},"coordinates":[301047,308939]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Valdivia","adm0name":"Chile","adm1name":"Los Ríos","iso_a2":"CL"},"coordinates":[296541,268953]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Concepcion","adm0name":"Chile","adm1name":"Bío-Bío","iso_a2":"CL"},"coordinates":[297083,286520]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Puerto Montt","adm0name":"Chile","adm1name":"Los Lagos","iso_a2":"CL"},"coordinates":[297416,259030]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Nuuk","adm0name":"Greenland","adm1name":"Kommuneqarfik Sermersooq","iso_a2":"GL"},"coordinates":[356298,885057]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Nouakchott","adm0name":"Mauritania","adm1name":"Nouakchott","iso_a2":"MR"},"coordinates":[455623,611869]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Bamako","adm0name":"Mali","adm1name":"Bamako","iso_a2":"ML"},"coordinates":[477771,579673]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Atar","adm0name":"Mauritania","adm1name":"Adrar","iso_a2":"MR"},"coordinates":[463750,626267]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Djenne","adm0name":"Mali","adm1name":"Mopti","iso_a2":"ML"},"coordinates":[487360,587067]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Sabha","adm0name":"Libya","adm1name":"Sabha","iso_a2":"LY"},"coordinates":[540092,664874]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Banghazi","adm0name":"Libya","adm1name":"Benghazi","iso_a2":"LY"},"coordinates":[555735,695002]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Thessaloniki","adm0name":"Greece","adm1name":"Kentriki Makedonia","iso_a2":"GR"},"coordinates":[563564,745831]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Beirut","adm0name":"Lebanon","adm1name":"Beirut","iso_a2":"LB"},"coordinates":[598632,705401]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Tbilisi","adm0name":"Georgia","adm1name":"Tbilisi","iso_a2":"GE"},"coordinates":[624412,751926]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Gonder","adm0name":"Ethiopia","adm1name":"Amhara","iso_a2":"ET"},"coordinates":[604055,579425]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Astana","adm0name":"Kazakhstan","adm1name":"Aqmola","iso_a2":"KZ"},"coordinates":[698409,807937]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Qaraghandy","adm0name":"Kazakhstan","adm1name":"Qaraghandy","iso_a2":"KZ"},"coordinates":[703096,800258]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Almaty","adm0name":"Kazakhstan","adm1name":"Almaty","iso_a2":"KZ"},"coordinates":[713646,761406]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Isfahan","adm0name":"Iran","adm1name":"Esfahan","iso_a2":"IR"},"coordinates":[643606,698458]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Shiraz","adm0name":"Iran","adm1name":"Fars","iso_a2":"IR"},"coordinates":[646022,680270]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Amritsar","adm0name":"India","adm1name":"Punjab","iso_a2":"IN"},"coordinates":[707966,692178]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Varanasi","adm0name":"India","adm1name":"Uttar Pradesh","iso_a2":"IN"},"coordinates":[730549,654795]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Asansol","adm0name":"India","adm1name":"West Bengal","iso_a2":"IN"},"coordinates":[741614,645039]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Bhilai","adm0name":"India","adm1name":"Chhattisgarh","iso_a2":"IN"},"coordinates":[726197,630426]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Bhopal","adm0name":"India","adm1name":"Madhya Pradesh","iso_a2":"IN"},"coordinates":[715022,642472]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Madurai","adm0name":"India","adm1name":"Tamil Nadu","iso_a2":"IN"},"coordinates":[716994,563499]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Coimbatore","adm0name":"India","adm1name":"Tamil Nadu","iso_a2":"IN"},"coordinates":[713744,569897]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Vientiane","adm0name":"Laos","adm1name":"Vientiane [prefecture]","iso_a2":"LA"},"coordinates":[784999,611160]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Brazzaville","adm0name":"Congo (Brazzaville)","adm1name":"Pool","iso_a2":"CG"},"coordinates":[542451,479495]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Conakry","adm0name":"Guinea","adm1name":"Conakry","iso_a2":"GN"},"coordinates":[461993,561198]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Yamoussoukro","adm0name":"Ivory Coast","adm1name":"Lacs","iso_a2":"CI"},"coordinates":[485346,545112]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Cruzeiro do Sul","adm0name":"Brazil","adm1name":"Acre","iso_a2":"BR"},"coordinates":[298138,459513]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Leticia","adm0name":"Colombia","adm1name":"Amazonas","iso_a2":"CO"},"coordinates":[305679,479827]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Manaus","adm0name":"Brazil","adm1name":"Amazonas","iso_a2":"BR"},"coordinates":[333328,486363]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Caxias","adm0name":"Brazil","adm1name":"Maranhão","iso_a2":"BR"},"coordinates":[379583,476084]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Santarem","adm0name":"Brazil","adm1name":"Pará","iso_a2":"BR"},"coordinates":[348055,490302]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Maraba","adm0name":"Brazil","adm1name":"Pará","iso_a2":"BR"},"coordinates":[363566,473022]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Vilhena","adm0name":"Brazil","adm1name":"Rondônia","iso_a2":"BR"},"coordinates":[333009,429378]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Ji-Parana","adm0name":"Brazil","adm1name":"Rondônia","iso_a2":"BR"},"coordinates":[327870,440536]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Campo Grande","adm0name":"Brazil","adm1name":"Mato Grosso do Sul","iso_a2":"BR"},"coordinates":[348282,383573]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Florianopolis","adm0name":"Brazil","adm1name":"Santa Catarina","iso_a2":"BR"},"coordinates":[365216,341333]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Feira de Santana","adm0name":"Brazil","adm1name":"Bahia","iso_a2":"BR"},"coordinates":[391750,432142]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Winnipeg","adm0name":"Canada","adm1name":"Manitoba","iso_a2":"CA"},"coordinates":[230094,800247]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Churchill","adm0name":"Canada","adm1name":"Manitoba","iso_a2":"CA"},"coordinates":[238427,852873]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Regina","adm0name":"Canada","adm1name":"Saskatchewan","iso_a2":"CA"},"coordinates":[209397,803606]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Saskatoon","adm0name":"Canada","adm1name":"Saskatchewan","iso_a2":"CA"},"coordinates":[203694,813796]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Calgary","adm0name":"Canada","adm1name":"Alberta","iso_a2":"CA"},"coordinates":[183106,807367]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Prince Rupert","adm0name":"Canada","adm1name":"British Columbia","iso_a2":"CA"},"coordinates":[137973,826514]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Victoria","adm0name":"Canada","adm1name":"British Columbia","iso_a2":"CA"},"coordinates":[157361,791657]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Arctic Bay","adm0name":"Canada","adm1name":"Nunavut","iso_a2":"CA"},"coordinates":[263425,937400]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Resolute","adm0name":"Canada","adm1name":"Nunavut","iso_a2":"CA"},"coordinates":[236389,947175]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Repulse Bay","adm0name":"Canada","adm1name":"Nunavut","iso_a2":"CA"},"coordinates":[260325,898868]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Yellowknife","adm0name":"Canada","adm1name":"Northwest Territories","iso_a2":"CA"},"coordinates":[182230,874652]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Fort Good Hope","adm0name":"Canada","adm1name":"Northwest Territories","iso_a2":"CA"},"coordinates":[142685,897311]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Whitehorse","adm0name":"Canada","adm1name":"Yukon","iso_a2":"CA"},"coordinates":[124861,864430]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Boa Vista","adm0name":"Brazil","adm1name":"Roraima","iso_a2":"BR"},"coordinates":[331483,521401]},{"type":"Point","properties":{"scalerank":3,"labelrank":1,"name":"Macapá","adm0name":"Brazil","adm1name":"Amapá","iso_a2":"BR"},"coordinates":[358194,504913]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Ottawa","adm0name":"Canada","adm1name":"Ontario","iso_a2":"CA"},"coordinates":[289716,773798]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Fort Severn","adm0name":"Canada","adm1name":"Ontario","iso_a2":"CA"},"coordinates":[256528,836387]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Thunder Bay","adm0name":"Canada","adm1name":"Ontario","iso_a2":"CA"},"coordinates":[252014,791734]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Québec","adm0name":"Canada","adm1name":"Québec","iso_a2":"CA"},"coordinates":[302095,782218]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Halifax","adm0name":"Canada","adm1name":"Nova Scotia","iso_a2":"CA"},"coordinates":[323333,769244]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"St. John’s","adm0name":"Canada","adm1name":"Newfoundland and Labrador","iso_a2":"CA"},"coordinates":[353663,786632]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Nain","adm0name":"Canada","adm1name":"Newfoundland and Labrador","iso_a2":"CA"},"coordinates":[328650,839729]},{"type":"Point","properties":{"scalerank":3,"labelrank":2,"name":"Charlottetown","adm0name":"Canada","adm1name":"Prince Edward Island","iso_a2":"CA"},"coordinates":[324635,778719]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Ndele","adm0name":"Central African Republic","adm1name":"Bamingui-Bangoran","iso_a2":"CF"},"coordinates":[557369,554537]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Belgrade","adm0name":"Serbia","adm1name":"Grad Beograd","iso_a2":"RS"},"coordinates":[556849,770254]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Obo","adm0name":"Central African Republic","adm1name":"Haut-Mbomou","iso_a2":"CF"},"coordinates":[573611,536709]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Bandar Seri Begawan","adm0name":"Brunei","adm1name":"Brunei and Muara","iso_a2":"BN"},"coordinates":[819259,533648]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Puerto Deseado","adm0name":"Argentina","adm1name":"Santa Cruz","iso_a2":"AR"},"coordinates":[316944,221825]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Rio Gallegos","adm0name":"Argentina","adm1name":"Santa Cruz","iso_a2":"AR"},"coordinates":[307731,198818]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Comodoro Rivadavia","adm0name":"Argentina","adm1name":"Chubut","iso_a2":"AR"},"coordinates":[312500,232962]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Mendoza","adm0name":"Argentina","adm1name":"Mendoza","iso_a2":"AR"},"coordinates":[308837,309913]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Sucre","adm0name":"Bolivia","adm1name":"Chuquisaca","iso_a2":"BO"},"coordinates":[318723,391910]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Riberalta","adm0name":"Bolivia","adm1name":"El Beni","iso_a2":"BO"},"coordinates":[316388,439649]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Bahia Blanca","adm0name":"Argentina","adm1name":"Ciudad de Buenos Aires","iso_a2":"AR"},"coordinates":[327041,275203]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Mar del Plata","adm0name":"Argentina","adm1name":"Ciudad de Buenos Aires","iso_a2":"AR"},"coordinates":[340055,279587]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Córdoba","adm0name":"Argentina","adm1name":"Córdoba","iso_a2":"AR"},"coordinates":[321710,318701]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Posadas","adm0name":"Argentina","adm1name":"Misiones","iso_a2":"AR"},"coordinates":[344763,342637]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Belmopan","adm0name":"Belize","adm1name":"Cayo","iso_a2":"BZ"},"coordinates":[253425,606926]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Bangui","adm0name":"Central African Republic","adm1name":"Bangui","iso_a2":"CF"},"coordinates":[551550,530587]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Maroua","adm0name":"Cameroon","adm1name":"Extrême-Nord","iso_a2":"CM"},"coordinates":[539790,567490]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Yaounde","adm0name":"Cameroon","adm1name":"Centre","iso_a2":"CM"},"coordinates":[531985,527636]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Tirana","adm0name":"Albania","adm1name":"Durrës","iso_a2":"AL"},"coordinates":[555051,749560]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Yerevan","adm0name":"Armenia","adm1name":"Erevan","iso_a2":"AM"},"coordinates":[623642,742780]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Baku","adm0name":"Azerbaijan","adm1name":"Baki","iso_a2":"AZ"},"coordinates":[638500,744048]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Kandahar","adm0name":"Afghanistan","adm1name":"Kandahar","iso_a2":"AF"},"coordinates":[682485,691989]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Phnom Penh","adm0name":"Cambodia","adm1name":"Phnom Penh","iso_a2":"KH"},"coordinates":[791428,573156]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Menongue","adm0name":"Angola","adm1name":"Cuando Cubango","iso_a2":"AO"},"coordinates":[549166,417825]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Huambo","adm0name":"Angola","adm1name":"Huambo","iso_a2":"AO"},"coordinates":[543772,429192]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"La Paz","adm0name":"Bolivia","adm1name":"La Paz","iso_a2":"BO"},"coordinates":[310688,406987]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Santa Cruz","adm0name":"Bolivia","adm1name":"Santa Cruz","iso_a2":"BO"},"coordinates":[324366,399546]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Oran","adm0name":"Algeria","adm1name":"Oran","iso_a2":"DZ"},"coordinates":[498272,716291]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Cotonou","adm0name":"Benin","adm1name":"Ouémé","iso_a2":"BJ"},"coordinates":[506994,542646]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Tamanrasset","adm0name":"Algeria","adm1name":"Tamanghasset","iso_a2":"DZ"},"coordinates":[515341,639705]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Ghardaia","adm0name":"Algeria","adm1name":"Ghardaïa","iso_a2":"DZ"},"coordinates":[510194,697202]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Sofia","adm0name":"Bulgaria","adm1name":"Grad Sofiya","iso_a2":"BG"},"coordinates":[564762,757604]},{"type":"Point","properties":{"scalerank":3,"labelrank":6,"name":"Minsk","adm0name":"Belarus","adm1name":"Minsk","iso_a2":"BY"},"coordinates":[576568,824056]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Thimphu","adm0name":"Bhutan","adm1name":"Thimphu","iso_a2":"BT"},"coordinates":[748997,667480]},{"type":"Point","properties":{"scalerank":3,"labelrank":7,"name":"Gaborone","adm0name":"Botswana","adm1name":"South-East","iso_a2":"BW"},"coordinates":[571977,358701]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Darwin","adm0name":"Australia","adm1name":"Northern Territory","iso_a2":"AU"},"coordinates":[863471,431104]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Alice Springs","adm0name":"Australia","adm1name":"Northern Territory","iso_a2":"AU"},"coordinates":[871888,364302]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Canberra","adm0name":"Australia","adm1name":"Australian Capital Territory","iso_a2":"AU"},"coordinates":[914247,295685]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Newcastle","adm0name":"Australia","adm1name":"New South Wales","iso_a2":"AU"},"coordinates":[921708,310126]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Adelaide","adm0name":"Australia","adm1name":"South Australia","iso_a2":"AU"},"coordinates":[884994,297758]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Townsville","adm0name":"Australia","adm1name":"Queensland","iso_a2":"AU"},"coordinates":[907694,390672]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Brisbane","adm0name":"Australia","adm1name":"Queensland","iso_a2":"AU"},"coordinates":[925091,342073]},{"type":"Point","properties":{"scalerank":3,"labelrank":3,"name":"Hobart","adm0name":"Australia","adm1name":"Tasmania","iso_a2":"AU"},"coordinates":[909152,250854]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Ouagadougou","adm0name":"Burkina Faso","adm1name":"Kadiogo","iso_a2":"BF"},"coordinates":[495759,578016]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Sarajevo","adm0name":"Bosnia and Herzegovina","adm1name":"Sarajevo","iso_a2":"BA"},"coordinates":[551063,764505]},{"type":"Point","properties":{"scalerank":3,"labelrank":5,"name":"Naypyidaw","adm0name":"Myanmar","adm1name":"Mandalay","iso_a2":"MM"},"coordinates":[766990,621835]},{"type":"Point","properties":{"scalerank":3,"labelrank":0,"name":"San Juan","adm0name":"Puerto Rico","iso_a2":"PR"},"coordinates":[316305,613964]},{"type":"Point","properties":{"scalerank":3,"labelrank":0,"name":"Stanley","adm0name":"Falkland Islands","iso_a2":"FK"},"coordinates":[339306,198423]},{"type":"Point","properties":{"scalerank":3,"labelrank":0,"name":"Hamilton","adm0name":"Bermuda","iso_a2":"BM"},"coordinates":[320044,696043]},{"type":"Point","properties":{"scalerank":3,"labelrank":0,"name":"Nukualofa","adm0name":"Tonga","iso_a2":"TO"},"coordinates":[13276,379483]},{"type":"Point","properties":{"scalerank":3,"labelrank":0,"name":"Hargeysa","adm0name":"Somaliland","iso_a2":"-99"},"coordinates":[622403,561355]},{"type":"Point","properties":{"scalerank":3,"labelrank":0,"name":"Victoria","adm0name":"Seychelles","iso_a2":"SC"},"coordinates":[654027,477366]},{"type":"Point","properties":{"scalerank":3,"labelrank":0,"name":"Sao Tome","adm0name":"Sao Tome and Principe","iso_a2":"ST"},"coordinates":[518703,506692]},{"type":"Point","properties":{"scalerank":3,"labelrank":0,"name":"Apia","adm0name":"Samoa","iso_a2":"WS"},"coordinates":[22948,422713]},{"type":"Point","properties":{"scalerank":3,"labelrank":0,"name":"Valletta","adm0name":"Malta","iso_a2":"MT"},"coordinates":[540318,717403]},{"type":"Point","properties":{"scalerank":3,"labelrank":0,"name":"Male","adm0name":"Maldives","iso_a2":"MV"},"coordinates":[704166,529403]},{"type":"Point","properties":{"scalerank":3,"labelrank":0,"name":"Jerusalem","adm0name":"Israel","adm1name":"Jerusalem","iso_a2":"IL"},"coordinates":[597795,692987]},{"type":"Point","properties":{"scalerank":3,"labelrank":0,"name":"Praia","adm0name":"Cape Verde","iso_a2":"CV"},"coordinates":[434676,593090]},{"type":"Point","properties":{"scalerank":3,"labelrank":0,"name":"Nassau","adm0name":"The Bahamas","iso_a2":"BS"},"coordinates":[285138,653322]},{"type":"Point","properties":{"scalerank":3,"labelrank":0,"name":"Nicosia","adm0name":"Cyprus","iso_a2":"CY"},"coordinates":[592684,713060]},{"type":"Point","properties":{"scalerank":3,"labelrank":8,"name":"Kaohsiung","adm0name":"Taiwan","adm1name":"Kaohsiung City","iso_a2":"TW"},"coordinates":[834073,638807]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Shenzhen","adm0name":"China","adm1name":"Guangdong","iso_a2":"CN"},"coordinates":[816999,638339]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Zibo","adm0name":"China","adm1name":"Shandong","iso_a2":"CN"},"coordinates":[827911,722749]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Minneapolis","adm0name":"United States of America","adm1name":"Minnesota","iso_a2":"US"},"coordinates":[240962,771210]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Honolulu","adm0name":"United States of America","adm1name":"Hawaii","iso_a2":"US"},"coordinates":[61500,630960]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Seattle","adm0name":"United States of America","adm1name":"Washington","iso_a2":"US"},"coordinates":[160161,786555]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Phoenix","adm0name":"United States of America","adm1name":"Arizona","iso_a2":"US"},"coordinates":[188689,703435]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"San Diego","adm0name":"United States of America","adm1name":"California","iso_a2":"US"},"coordinates":[174494,699169]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"St. Louis","adm0name":"United States of America","adm1name":"Missouri","iso_a2":"US"},"coordinates":[249328,733620]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"New Orleans","adm0name":"United States of America","adm1name":"Louisiana","iso_a2":"US"},"coordinates":[249883,682432]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Dallas","adm0name":"United States of America","adm1name":"Texas","iso_a2":"US"},"coordinates":[230995,699169]},{"type":"Point","properties":{"scalerank":2,"labelrank":6,"name":"Maracaibo","adm0name":"Venezuela","adm1name":"Zulia","iso_a2":"VE"},"coordinates":[300939,568298]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Boston","adm0name":"United States of America","adm1name":"Massachusetts","iso_a2":"US"},"coordinates":[302578,755511]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Tampa","adm0name":"United States of America","adm1name":"Florida","iso_a2":"US"},"coordinates":[270943,670299]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Philadelphia","adm0name":"United States of America","adm1name":"Pennsylvania","iso_a2":"US"},"coordinates":[291189,741707]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Detroit","adm0name":"United States of America","adm1name":"Michigan","iso_a2":"US"},"coordinates":[269217,755511]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Anchorage","adm0name":"United States of America","adm1name":"Alaska","iso_a2":"US"},"coordinates":[83611,867412]},{"type":"Point","properties":{"scalerank":2,"labelrank":5,"name":"Hanoi","adm0name":"Vietnam","adm1name":"Thái Nguyên","iso_a2":"VN"},"coordinates":[794022,629340]},{"type":"Point","properties":{"scalerank":2,"labelrank":5,"name":"Ho Chi Minh City","adm0name":"Vietnam","adm1name":"H? Chí Minh city","iso_a2":"VN"},"coordinates":[796369,568595]},{"type":"Point","properties":{"scalerank":2,"labelrank":5,"name":"Ankara","adm0name":"Turkey","adm1name":"Ankara","iso_a2":"TR"},"coordinates":[591284,741276]},{"type":"Point","properties":{"scalerank":2,"labelrank":6,"name":"Budapest","adm0name":"Hungary","adm1name":"Budapest","iso_a2":"HU"},"coordinates":[553003,786140]},{"type":"Point","properties":{"scalerank":2,"labelrank":6,"name":"Sanaa","adm0name":"Yemen","adm1name":"Amanat Al Asimah","iso_a2":"YE"},"coordinates":[622791,595697]},{"type":"Point","properties":{"scalerank":2,"labelrank":3,"name":"Barcelona","adm0name":"Spain","adm1name":"Cataluña","iso_a2":"ES"},"coordinates":[506059,749902]},{"type":"Point","properties":{"scalerank":2,"labelrank":6,"name":"Bucharest","adm0name":"Romania","adm1name":"Bucharest","iso_a2":"RO"},"coordinates":[572494,767972]},{"type":"Point","properties":{"scalerank":2,"labelrank":6,"name":"Aleppo","adm0name":"Syria","adm1name":"Aleppo (Halab)","iso_a2":"SY"},"coordinates":[603244,719372]},{"type":"Point","properties":{"scalerank":2,"labelrank":6,"name":"Damascus","adm0name":"Syria","adm1name":"Damascus","iso_a2":"SY"},"coordinates":[600827,703198]},{"type":"Point","properties":{"scalerank":2,"labelrank":7,"name":"Zürich","adm0name":"Switzerland","adm1name":"Zürich","iso_a2":"CH"},"coordinates":[523744,785429]},{"type":"Point","properties":{"scalerank":2,"labelrank":6,"name":"Lisbon","adm0name":"Portugal","adm1name":"Lisboa","iso_a2":"PT"},"coordinates":[474591,734139]},{"type":"Point","properties":{"scalerank":2,"labelrank":5,"name":"Khartoum","adm0name":"Sudan","adm1name":"Khartoum","iso_a2":"SD"},"coordinates":[590367,597079]},{"type":"Point","properties":{"scalerank":2,"labelrank":5,"name":"Jeddah","adm0name":"Saudi Arabia","adm1name":"Makkah","iso_a2":"SA"},"coordinates":[608936,632204]},{"type":"Point","properties":{"scalerank":2,"labelrank":5,"name":"Makkah","adm0name":"Saudi Arabia","adm1name":"Makkah","iso_a2":"SA"},"coordinates":[610605,631690]},{"type":"Point","properties":{"scalerank":2,"labelrank":7,"name":"Oslo","adm0name":"Norway","adm1name":"Oslo","iso_a2":"NO"},"coordinates":[529855,859702]},{"type":"Point","properties":{"scalerank":2,"labelrank":2,"name":"Lahore","adm0name":"Pakistan","adm1name":"Punjab","iso_a2":"PK"},"coordinates":[706522,691704]},{"type":"Point","properties":{"scalerank":2,"labelrank":2,"name":"Karachi","adm0name":"Pakistan","adm1name":"Sind","iso_a2":"PK"},"coordinates":[686077,652070]},{"type":"Point","properties":{"scalerank":2,"labelrank":3,"name":"Durban","adm0name":"South Africa","adm1name":"KwaZulu-Natal","iso_a2":"ZA"},"coordinates":[586050,327795]},{"type":"Point","properties":{"scalerank":2,"labelrank":2,"name":"St. Petersburg","adm0name":"Russia","adm1name":"City of St. Petersburg","iso_a2":"RU"},"coordinates":[584205,859834]},{"type":"Point","properties":{"scalerank":2,"labelrank":2,"name":"Guadalajara","adm0name":"Mexico","adm1name":"Jalisco","iso_a2":"MX"},"coordinates":[212967,627187]},{"type":"Point","properties":{"scalerank":2,"labelrank":2,"name":"Puebla","adm0name":"Mexico","adm1name":"Puebla","iso_a2":"MX"},"coordinates":[227216,617589]},{"type":"Point","properties":{"scalerank":2,"labelrank":2,"name":"Kano","adm0name":"Nigeria","adm1name":"Kano","iso_a2":"NG"},"coordinates":[523661,575822]},{"type":"Point","properties":{"scalerank":2,"labelrank":5,"name":"Warsaw","adm0name":"Poland","adm1name":"Masovian","iso_a2":"PL"},"coordinates":[558328,814281]},{"type":"Point","properties":{"scalerank":2,"labelrank":6,"name":"Pyongyang","adm0name":"North Korea","adm1name":"P'yongyang","iso_a2":"KP"},"coordinates":[849312,735898]},{"type":"Point","properties":{"scalerank":2,"labelrank":5,"name":"Dar es Salaam","adm0name":"Tanzania","adm1name":"Dar-Es-Salaam","iso_a2":"TZ"},"coordinates":[609072,464442]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Medan","adm0name":"Indonesia","adm1name":"Sumatera Utara","iso_a2":"ID"},"coordinates":[774021,525938]},{"type":"Point","properties":{"scalerank":2,"labelrank":8,"name":"Dublin","adm0name":"Ireland","adm1name":"Dublin","iso_a2":"IE"},"coordinates":[482636,820698]},{"type":"Point","properties":{"scalerank":2,"labelrank":8,"name":"Monrovia","adm0name":"Liberia","adm1name":"Montserrado","iso_a2":"LR"},"coordinates":[470001,542128]},{"type":"Point","properties":{"scalerank":2,"labelrank":3,"name":"Naples","adm0name":"Italy","adm1name":"Campania","iso_a2":"IT"},"coordinates":[539564,746684]},{"type":"Point","properties":{"scalerank":2,"labelrank":3,"name":"Milan","adm0name":"Italy","adm1name":"Lombardia","iso_a2":"IT"},"coordinates":[525564,774114]},{"type":"Point","properties":{"scalerank":2,"labelrank":6,"name":"Kuala Lumpur","adm0name":"Malaysia","adm1name":"Selangor","iso_a2":"MY"},"coordinates":[782494,523489]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Lanzhou","adm0name":"China","adm1name":"Gansu","iso_a2":"CN"},"coordinates":[788304,718341]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Nanning","adm0name":"China","adm1name":"Guangxi","iso_a2":"CN"},"coordinates":[800883,639925]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Guiyang","adm0name":"China","adm1name":"Guizhou","iso_a2":"CN"},"coordinates":[796439,662201]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Chongqing","adm0name":"China","adm1name":"Chongqing","iso_a2":"CN"},"coordinates":[796091,679885]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Fuzhou","adm0name":"China","adm1name":"Fujian","iso_a2":"CN"},"coordinates":[831382,659239]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Guangzhou","adm0name":"China","adm1name":"Guangdong","iso_a2":"CN"},"coordinates":[814786,641850]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Dongguan","adm0name":"China","adm1name":"Guangdong","iso_a2":"CN"},"coordinates":[815951,641281]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Bandung","adm0name":"Indonesia","adm1name":"Jawa Barat","iso_a2":"ID"},"coordinates":[798799,463554]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Surabaya","adm0name":"Indonesia","adm1name":"Jawa Timur","iso_a2":"ID"},"coordinates":[813191,461781]},{"type":"Point","properties":{"scalerank":2,"labelrank":7,"name":"Guayaquil","adm0name":"Ecuador","adm1name":"Guayas","iso_a2":"EC"},"coordinates":[277994,491576]},{"type":"Point","properties":{"scalerank":2,"labelrank":5,"name":"Medellin","adm0name":"Colombia","adm1name":"Antioquia","iso_a2":"CO"},"coordinates":[290064,541905]},{"type":"Point","properties":{"scalerank":2,"labelrank":5,"name":"Cali","adm0name":"Colombia","adm1name":"Valle del Cauca","iso_a2":"CO"},"coordinates":[287494,524872]},{"type":"Point","properties":{"scalerank":2,"labelrank":6,"name":"Havana","adm0name":"Cuba","adm1name":"Ciudad de la Habana","iso_a2":"CU"},"coordinates":[271205,641773]},{"type":"Point","properties":{"scalerank":2,"labelrank":3,"name":"Alexandria","adm0name":"Egypt","adm1name":"Al Iskandariyah","iso_a2":"EG"},"coordinates":[583188,689572]},{"type":"Point","properties":{"scalerank":2,"labelrank":3,"name":"Frankfurt","adm0name":"Germany","adm1name":"Hessen","iso_a2":"DE"},"coordinates":[524097,801532]},{"type":"Point","properties":{"scalerank":2,"labelrank":3,"name":"Hamburg","adm0name":"Germany","adm1name":"Hamburg","iso_a2":"DE"},"coordinates":[527772,821983]},{"type":"Point","properties":{"scalerank":2,"labelrank":3,"name":"Munich","adm0name":"Germany","adm1name":"Bayern","iso_a2":"DE"},"coordinates":[532147,789872]},{"type":"Point","properties":{"scalerank":2,"labelrank":7,"name":"Prague","adm0name":"Czech Republic","adm1name":"Prague","iso_a2":"CZ"},"coordinates":[540177,801445]},{"type":"Point","properties":{"scalerank":2,"labelrank":8,"name":"Kuwait","adm0name":"Kuwait","adm1name":"Al Kuwayt","iso_a2":"KW"},"coordinates":[633267,678728]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Xian","adm0name":"China","adm1name":"Shaanxi","iso_a2":"CN"},"coordinates":[802479,707789]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Taiyuan","adm0name":"China","adm1name":"Shanxi","iso_a2":"CN"},"coordinates":[812619,729117]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Wuhan","adm0name":"China","adm1name":"Hubei","iso_a2":"CN"},"coordinates":[817411,685898]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Changsha","adm0name":"China","adm1name":"Hunan","iso_a2":"CN"},"coordinates":[813800,671798]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Kunming","adm0name":"China","adm1name":"Yunnan","iso_a2":"CN"},"coordinates":[785216,653255]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Zhengzhou","adm0name":"China","adm1name":"Henan","iso_a2":"CN"},"coordinates":[815730,710633]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Shenyeng","adm0name":"China","adm1name":"Liaoning","iso_a2":"CN"},"coordinates":[842910,752400]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Jinan","adm0name":"China","adm1name":"Shandong","iso_a2":"CN"},"coordinates":[824980,722008]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Tianjin","adm0name":"China","adm1name":"Tianjin","iso_a2":"CN"},"coordinates":[825549,736553]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Nanchang","adm0name":"China","adm1name":"Jiangxi","iso_a2":"CN"},"coordinates":[821883,674642]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Nanjing","adm0name":"China","adm1name":"Jiangsu","iso_a2":"CN"},"coordinates":[829938,694608]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Hangzhou","adm0name":"China","adm1name":"Zhejiang","iso_a2":"CN"},"coordinates":[833799,683943]},{"type":"Point","properties":{"scalerank":2,"labelrank":2,"name":"Hiroshima","adm0name":"Japan","adm1name":"Hiroshima","iso_a2":"JP"},"coordinates":[867890,708458]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Changchun","adm0name":"China","adm1name":"Jilin","iso_a2":"CN"},"coordinates":[848161,764605]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Baotou","adm0name":"China","adm1name":"Nei Mongol","iso_a2":"CN"},"coordinates":[805055,745571]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Harbin","adm0name":"China","adm1name":"Heilongjiang","iso_a2":"CN"},"coordinates":[851800,775772]},{"type":"Point","properties":{"scalerank":2,"labelrank":2,"name":"Sapporo","adm0name":"Japan","adm1name":"Hokkaido","iso_a2":"JP"},"coordinates":[892605,759924]},{"type":"Point","properties":{"scalerank":2,"labelrank":8,"name":"Santo Domingo","adm0name":"Dominican Republic","adm1name":"Distrito Nacional","iso_a2":"DO"},"coordinates":[305828,614153]},{"type":"Point","properties":{"scalerank":2,"labelrank":6,"name":"Accra","adm0name":"Ghana","adm1name":"Greater Accra","iso_a2":"GH"},"coordinates":[499392,537610]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Delhi","adm0name":"India","adm1name":"Delhi","iso_a2":"IN"},"coordinates":[714522,674583]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Hyderabad","adm0name":"India","adm1name":"Andhra Pradesh","iso_a2":"IN"},"coordinates":[717993,607814]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Pune","adm0name":"India","adm1name":"Maharashtra","iso_a2":"IN"},"coordinates":[705133,614509]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Nagpur","adm0name":"India","adm1name":"Maharashtra","iso_a2":"IN"},"coordinates":[719688,630149]},{"type":"Point","properties":{"scalerank":2,"labelrank":7,"name":"Tripoli","adm0name":"Libya","adm1name":"Tajura' wa an Nawahi al Arba","iso_a2":"LY"},"coordinates":[536611,699587]},{"type":"Point","properties":{"scalerank":2,"labelrank":8,"name":"Tel Aviv-Yafo","adm0name":"Israel","adm1name":"Tel Aviv","iso_a2":"IL"},"coordinates":[596577,694785]},{"type":"Point","properties":{"scalerank":2,"labelrank":7,"name":"Helsinki","adm0name":"Finland","adm1name":"Southern Finland","iso_a2":"FI"},"coordinates":[569256,861236]},{"type":"Point","properties":{"scalerank":2,"labelrank":5,"name":"Mashhad","adm0name":"Iran","adm1name":"Razavi Khorasan","iso_a2":"IR"},"coordinates":[665466,719608]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Jaipur","adm0name":"India","adm1name":"Rajasthan","iso_a2":"IN"},"coordinates":[710577,664222]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Kanpur","adm0name":"India","adm1name":"Uttar Pradesh","iso_a2":"IN"},"coordinates":[723105,661490]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Patna","adm0name":"India","adm1name":"Bihar","iso_a2":"IN"},"coordinates":[736466,656543]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Chennai","adm0name":"India","adm1name":"Tamil Nadu","iso_a2":"IN"},"coordinates":[722994,582279]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Ahmedabad","adm0name":"India","adm1name":"Dadra and Nagar Haveli","iso_a2":"IN"},"coordinates":[701605,641169]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Surat","adm0name":"India","adm1name":"Dadra and Nagar Haveli","iso_a2":"IN"},"coordinates":[702327,630327]},{"type":"Point","properties":{"scalerank":2,"labelrank":8,"name":"København","adm0name":"Denmark","adm1name":"Hovedstaden","iso_a2":"DK"},"coordinates":[534893,834594]},{"type":"Point","properties":{"scalerank":2,"labelrank":8,"name":"Abidjan","adm0name":"Ivory Coast","adm1name":"Lagunes","iso_a2":"CI"},"coordinates":[488772,536247]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Belem","adm0name":"Brazil","adm1name":"Pará","iso_a2":"BR"},"coordinates":[365327,496138]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Brasilia","adm0name":"Brazil","adm1name":"Distrito Federal","iso_a2":"BR"},"coordinates":[366894,411221]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Porto Alegre","adm0name":"Brazil","adm1name":"Rio Grande do Sul","iso_a2":"BR"},"coordinates":[357772,326699]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Curitiba","adm0name":"Brazil","adm1name":"Paraná","iso_a2":"BR"},"coordinates":[362994,354129]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Fortaleza","adm0name":"Brazil","adm1name":"Ceará","iso_a2":"BR"},"coordinates":[392828,482512]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Salvador","adm0name":"Brazil","adm1name":"Bahia","iso_a2":"BR"},"coordinates":[393106,427888]},{"type":"Point","properties":{"scalerank":2,"labelrank":2,"name":"Edmonton","adm0name":"Canada","adm1name":"Alberta","iso_a2":"CA"},"coordinates":[184717,821983]},{"type":"Point","properties":{"scalerank":2,"labelrank":2,"name":"Montréal","adm0name":"Canada","adm1name":"Québec","iso_a2":"CA"},"coordinates":[295596,774292]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Goiania","adm0name":"Brazil","adm1name":"Goiás","iso_a2":"BR"},"coordinates":[363050,405672]},{"type":"Point","properties":{"scalerank":2,"labelrank":1,"name":"Recife","adm0name":"Brazil","adm1name":"Pernambuco","iso_a2":"BR"},"coordinates":[403006,456885]},{"type":"Point","properties":{"scalerank":2,"labelrank":8,"name":"Brussels","adm0name":"Belgium","adm1name":"Brussels","iso_a2":"BE"},"coordinates":[512031,805888]},{"type":"Point","properties":{"scalerank":2,"labelrank":2,"name":"Dhaka","adm0name":"Bangladesh","adm1name":"Dhaka","iso_a2":"BD"},"coordinates":[751129,645275]},{"type":"Point","properties":{"scalerank":2,"labelrank":6,"name":"Luanda","adm0name":"Angola","adm1name":"Luanda","iso_a2":"AO"},"coordinates":[536756,452367]},{"type":"Point","properties":{"scalerank":2,"labelrank":5,"name":"Algiers","adm0name":"Algeria","adm1name":"Alger","iso_a2":"DZ"},"coordinates":[508468,722530]},{"type":"Point","properties":{"scalerank":2,"labelrank":2,"name":"Chittagong","adm0name":"Bangladesh","adm1name":"Chittagong","iso_a2":"BD"},"coordinates":[754993,637022]},{"type":"Point","properties":{"scalerank":2,"labelrank":3,"name":"Perth","adm0name":"Australia","adm1name":"Western Australia","iso_a2":"AU"},"coordinates":[821772,315413]},{"type":"Point","properties":{"scalerank":2,"labelrank":5,"name":"Rangoon","adm0name":"Myanmar","adm1name":"Yangon","iso_a2":"MM"},"coordinates":[767123,604161]},{"type":"Point","properties":{"scalerank":1,"labelrank":1,"name":"San Francisco","adm0name":"United States of America","adm1name":"California","iso_a2":"US"},"coordinates":[159952,728479]},{"type":"Point","properties":{"scalerank":1,"labelrank":1,"name":"Denver","adm0name":"United States of America","adm1name":"Colorado","iso_a2":"US"},"coordinates":[208372,740162]},{"type":"Point","properties":{"scalerank":1,"labelrank":1,"name":"Houston","adm0name":"United States of America","adm1name":"Texas","iso_a2":"US"},"coordinates":[235161,681396]},{"type":"Point","properties":{"scalerank":1,"labelrank":1,"name":"Miami","adm0name":"United States of America","adm1name":"Florida","iso_a2":"US"},"coordinates":[277150,657506]},{"type":"Point","properties":{"scalerank":1,"labelrank":1,"name":"Atlanta","adm0name":"United States of America","adm1name":"Georgia","iso_a2":"US"},"coordinates":[265550,705153]},{"type":"Point","properties":{"scalerank":1,"labelrank":1,"name":"Chicago","adm0name":"United States of America","adm1name":"Illinois","iso_a2":"US"},"coordinates":[256244,752549]},{"type":"Point","properties":{"scalerank":1,"labelrank":6,"name":"Caracas","adm0name":"Venezuela","adm1name":"Distrito Capital","iso_a2":"VE"},"coordinates":[314114,566941]},{"type":"Point","properties":{"scalerank":1,"labelrank":5,"name":"Kiev","adm0name":"Ukraine","adm1name":"Kiev","iso_a2":"UA"},"coordinates":[584762,803519]},{"type":"Point","properties":{"scalerank":1,"labelrank":8,"name":"Dubai","adm0name":"United Arab Emirates","adm1name":"Dubay","iso_a2":"AE"},"coordinates":[653549,654203]},{"type":"Point","properties":{"scalerank":1,"labelrank":6,"name":"Tashkent","adm0name":"Uzbekistan","adm1name":"Tashkent","iso_a2":"UZ"},"coordinates":[692480,749478]},{"type":"Point","properties":{"scalerank":1,"labelrank":3,"name":"Madrid","adm0name":"Spain","adm1name":"Comunidad de Madrid","iso_a2":"ES"},"coordinates":[489762,744077]},{"type":"Point","properties":{"scalerank":1,"labelrank":7,"name":"Geneva","adm0name":"Switzerland","adm1name":"Genève","iso_a2":"CH"},"coordinates":[517055,778486]},{"type":"Point","properties":{"scalerank":1,"labelrank":7,"name":"Stockholm","adm0name":"Sweden","adm1name":"Stockholm","iso_a2":"SE"},"coordinates":[550264,856349]},{"type":"Point","properties":{"scalerank":1,"labelrank":5,"name":"Bangkok","adm0name":"Thailand","adm1name":"Bangkok Metropolis","iso_a2":"TH"},"coordinates":[779207,586190]},{"type":"Point","properties":{"scalerank":1,"labelrank":5,"name":"Lima","adm0name":"Peru","adm1name":"Lima","iso_a2":"PE"},"coordinates":[285967,433351]},{"type":"Point","properties":{"scalerank":1,"labelrank":8,"name":"Dakar","adm0name":"Senegal","adm1name":"Dakar","iso_a2":"SM"},"coordinates":[451458,591912]},{"type":"Point","properties":{"scalerank":1,"labelrank":3,"name":"Johannesburg","adm0name":"South Africa","adm1name":"Gauteng","iso_a2":"ZA"},"coordinates":[577855,349685]},{"type":"Point","properties":{"scalerank":1,"labelrank":8,"name":"Amsterdam","adm0name":"Netherlands","adm1name":"Noord-Holland","iso_a2":"NL"},"coordinates":[513651,814873]},{"type":"Point","properties":{"scalerank":1,"labelrank":5,"name":"Casablanca","adm0name":"Morocco","adm1name":"Grand Casablanca","iso_a2":"MA"},"coordinates":[478837,703790]},{"type":"Point","properties":{"scalerank":1,"labelrank":3,"name":"Seoul","adm0name":"South Korea","adm1name":"Seoul","iso_a2":"KR"},"coordinates":[852771,727288]},{"type":"Point","properties":{"scalerank":1,"labelrank":5,"name":"Manila","adm0name":"Philippines","adm1name":"Metropolitan Manila","iso_a2":"PH"},"coordinates":[836056,591250]},{"type":"Point","properties":{"scalerank":1,"labelrank":2,"name":"Monterrey","adm0name":"Mexico","adm1name":"Nuevo León","iso_a2":"MX"},"coordinates":[221300,656809]},{"type":"Point","properties":{"scalerank":1,"labelrank":8,"name":"Auckland","adm0name":"New Zealand","adm1name":"Auckland","iso_a2":"NZ"},"coordinates":[985452,286413]},{"type":"Point","properties":{"scalerank":1,"labelrank":3,"name":"Berlin","adm0name":"Germany","adm1name":"Berlin","iso_a2":"DE"},"coordinates":[537221,815891]},{"type":"Point","properties":{"scalerank":1,"labelrank":1,"name":"Urumqi","adm0name":"China","adm1name":"Xinjiang Uygur","iso_a2":"CN"},"coordinates":[743258,764249]},{"type":"Point","properties":{"scalerank":1,"labelrank":1,"name":"Chengdu","adm0name":"China","adm1name":"Sichuan","iso_a2":"CN"},"coordinates":[789078,686432]},{"type":"Point","properties":{"scalerank":1,"labelrank":2,"name":"Osaka","adm0name":"Japan","adm1name":"Osaka","iso_a2":"JP"},"coordinates":[876272,710604]},{"type":"Point","properties":{"scalerank":1,"labelrank":3,"name":"Kinshasa","adm0name":"Congo (Kinshasa)","adm1name":"Kinshasa City","iso_a2":"CD"},"coordinates":[542536,479077]},{"type":"Point","properties":{"scalerank":1,"labelrank":1,"name":"New Delhi","adm0name":"India","adm1name":"Delhi","iso_a2":"IN"},"coordinates":[714444,674156]},{"type":"Point","properties":{"scalerank":1,"labelrank":1,"name":"Bangalore","adm0name":"India","adm1name":"Karnataka","iso_a2":"IN"},"coordinates":[715438,581569]},{"type":"Point","properties":{"scalerank":1,"labelrank":6,"name":"Athens","adm0name":"Greece","adm1name":"Attiki","iso_a2":"GR"},"coordinates":[565920,729759]},{"type":"Point","properties":{"scalerank":1,"labelrank":5,"name":"Baghdad","adm0name":"Iraq","adm1name":"Baghdad","iso_a2":"IQ"},"coordinates":[623310,702242]},{"type":"Point","properties":{"scalerank":1,"labelrank":3,"name":"Addis Ababa","adm0name":"Ethiopia","adm1name":"Addis Ababa","iso_a2":"ET"},"coordinates":[607494,558246]},{"type":"Point","properties":{"scalerank":1,"labelrank":5,"name":"Tehran","adm0name":"Iran","adm1name":"Tehran","iso_a2":"IR"},"coordinates":[642839,716065]},{"type":"Point","properties":{"scalerank":1,"labelrank":2,"name":"Vancouver","adm0name":"Canada","adm1name":"British Columbia","iso_a2":"CA"},"coordinates":[157990,796647]},{"type":"Point","properties":{"scalerank":1,"labelrank":2,"name":"Toronto","adm0name":"Canada","adm1name":"Ontario","iso_a2":"CA"},"coordinates":[279383,763627]},{"type":"Point","properties":{"scalerank":1,"labelrank":3,"name":"Buenos Aires","adm0name":"Argentina","adm1name":"Ciudad de Buenos Aires","iso_a2":"AR"},"coordinates":[337779,299727]},{"type":"Point","properties":{"scalerank":1,"labelrank":5,"name":"Kabul","adm0name":"Afghanistan","adm1name":"Kabul","iso_a2":"AF"},"coordinates":[692169,709221]},{"type":"Point","properties":{"scalerank":1,"labelrank":7,"name":"Vienna","adm0name":"Austria","adm1name":"Wien","iso_a2":"AT"},"coordinates":[545457,790287]},{"type":"Point","properties":{"scalerank":1,"labelrank":3,"name":"Melbourne","adm0name":"Australia","adm1name":"Victoria","iso_a2":"AU"},"coordinates":[902702,280666]},{"type":"Point","properties":{"scalerank":1,"labelrank":8,"name":"Taipei","adm0name":"Taiwan","adm1name":"Taipei City","iso_a2":"TW"},"coordinates":[837688,653040]},{"type":"Point","properties":{"scalerank":0,"labelrank":1,"name":"Los Angeles","adm0name":"United States of America","adm1name":"California","iso_a2":"US"},"coordinates":[171717,706100]},{"type":"Point","properties":{"scalerank":0,"labelrank":1,"name":"Washington, D.C.","adm0name":"United States of America","adm1name":"District of Columbia","iso_a2":"US"},"coordinates":[286079,735187]},{"type":"Point","properties":{"scalerank":0,"labelrank":1,"name":"New York","adm0name":"United States of America","adm1name":"New York","iso_a2":"US"},"coordinates":[294495,746150]},{"type":"Point","properties":{"scalerank":0,"labelrank":5,"name":"London","adm0name":"United Kingdom","adm1name":"Westminster","iso_a2":"GB"},"coordinates":[499670,809838]},{"type":"Point","properties":{"scalerank":0,"labelrank":5,"name":"Istanbul","adm0name":"Turkey","adm1name":"Istanbul","iso_a2":"TR"},"coordinates":[580577,748253]},{"type":"Point","properties":{"scalerank":0,"labelrank":5,"name":"Riyadh","adm0name":"Saudi Arabia","adm1name":"Ar Riyad","iso_a2":"SA"},"coordinates":[629918,650712]},{"type":"Point","properties":{"scalerank":0,"labelrank":3,"name":"Cape Town","adm0name":"South Africa","adm1name":"Western Cape","iso_a2":"ZA"},"coordinates":[551202,303771]},{"type":"Point","properties":{"scalerank":0,"labelrank":2,"name":"Moscow","adm0name":"Russia","adm1name":"Moskva","iso_a2":"RU"},"coordinates":[604482,835030]},{"type":"Point","properties":{"scalerank":0,"labelrank":2,"name":"Mexico City","adm0name":"Mexico","adm1name":"Distrito Federal","iso_a2":"MX"},"coordinates":[224631,619915]},{"type":"Point","properties":{"scalerank":0,"labelrank":2,"name":"Lagos","adm0name":"Nigeria","adm1name":"Lagos","iso_a2":"NG"},"coordinates":[509415,542902]},{"type":"Point","properties":{"scalerank":0,"labelrank":3,"name":"Rome","adm0name":"Italy","adm1name":"Lazio","iso_a2":"IT"},"coordinates":[534670,752939]},{"type":"Point","properties":{"scalerank":0,"labelrank":1,"name":"Beijing","adm0name":"China","adm1name":"Beijing","iso_a2":"CN"},"coordinates":[823294,741286]},{"type":"Point","properties":{"scalerank":0,"labelrank":5,"name":"Nairobi","adm0name":"Kenya","adm1name":"Nairobi","iso_a2":"KE"},"coordinates":[602262,497125]},{"type":"Point","properties":{"scalerank":0,"labelrank":1,"name":"Jakarta","adm0name":"Indonesia","adm1name":"Jakarta Raya","iso_a2":"ID"},"coordinates":[796742,468148]},{"type":"Point","properties":{"scalerank":0,"labelrank":5,"name":"Bogota","adm0name":"Colombia","adm1name":"Bogota","iso_a2":"CO"},"coordinates":[294207,531960]},{"type":"Point","properties":{"scalerank":0,"labelrank":3,"name":"Cairo","adm0name":"Egypt","adm1name":"Al Qahirah","iso_a2":"EG"},"coordinates":[586799,682758]},{"type":"Point","properties":{"scalerank":0,"labelrank":1,"name":"Shanghai","adm0name":"China","adm1name":"Shanghai","iso_a2":"CN"},"coordinates":[837317,689669]},{"type":"Point","properties":{"scalerank":0,"labelrank":2,"name":"Tokyo","adm0name":"Japan","adm1name":"Tokyo","iso_a2":"JP"},"coordinates":[888192,716142]},{"type":"Point","properties":{"scalerank":0,"labelrank":1,"name":"Mumbai","adm0name":"India","adm1name":"Maharashtra","iso_a2":"IN"},"coordinates":[702374,617394]},{"type":"Point","properties":{"scalerank":0,"labelrank":3,"name":"Paris","adm0name":"France","adm1name":"Île-de-France","iso_a2":"FR"},"coordinates":[506476,794237]},{"type":"Point","properties":{"scalerank":0,"labelrank":3,"name":"Santiago","adm0name":"Chile","adm1name":"Región Metropolitana de Santiago","iso_a2":"CL"},"coordinates":[303697,306556]},{"type":"Point","properties":{"scalerank":0,"labelrank":1,"name":"Kolkata","adm0name":"India","adm1name":"West Bengal","iso_a2":"IN"},"coordinates":[745340,637999]},{"type":"Point","properties":{"scalerank":0,"labelrank":1,"name":"Rio de Janeiro","adm0name":"Brazil","adm1name":"Rio de Janeiro","iso_a2":"BR"},"coordinates":[379925,368910]},{"type":"Point","properties":{"scalerank":0,"labelrank":1,"name":"Sao Paulo","adm0name":"Brazil","adm1name":"São Paulo","iso_a2":"BR"},"coordinates":[370480,365156]},{"type":"Point","properties":{"scalerank":0,"labelrank":3,"name":"Sydney","adm0name":"Australia","adm1name":"New South Wales","iso_a2":"AU"},"coordinates":[919952,303771]},{"type":"Point","properties":{"scalerank":0,"labelrank":0,"name":"Singapore","adm0name":"Singapore","iso_a2":"SG"},"coordinates":[788482,512389]},{"type":"Point","properties":{"scalerank":0,"labelrank":0,"name":"Hong Kong","adm0name":"Hong Kong S.A.R.","iso_a2":"HK"},"coordinates":[817174,636873]}]}},"arcs":[[[85470,47819],[-4327,823],[2549,633],[1778,-1456]],[[135926,62848],[-3045,936],[3281,69],[-236,-1005]],[[54258,21344],[-9164,857],[3638,778],[5526,-1635]],[[965673,41772],[-3773,-978],[-481,1285],[4254,-307]],[[964121,46628],[6302,-1202],[-5445,-735],[-1844,-1198],[-1422,1933],[2409,1202]],[[405738,34787],[-7075,156],[2628,1209],[4447,-1365]],[[313161,34004],[-3575,90],[1434,1245],[2141,-1335]],[[334072,28722],[-2358,-3579],[-10237,1183],[-4755,2157],[12074,239],[-141,1922],[5030,1437],[387,-3359]],[[316184,30299],[-4542,3151],[4985,-464],[1416,-1955],[-1859,-732]],[[58185,31902],[-3469,-220],[-10900,3103],[279,1928],[2416,1619],[4480,-1060],[5441,-2972],[1753,-2398]],[[374381,37806],[4168,-45],[2102,-3902],[-1562,-4233],[-15721,-2674],[-1627,-838],[-12193,-508],[-513,1781],[2648,2728],[3047,-191],[5438,3920],[-1094,1165],[1427,4014],[3162,3306],[6265,1552],[3599,-570],[4781,-2400],[-384,-1842],[-3543,-1263]],[[304627,32657],[-4026,1396],[3443,3321],[8515,3087],[2085,-125],[-7008,-4897],[-3009,-2782]],[[146206,62619],[-2132,1749],[2510,-580],[-378,-1169]],[[149084,70536],[2927,-2396],[3427,-1125],[571,-2044],[-2879,102],[-6552,2931],[-99,2426],[2605,106]],[[442757,66979],[329,-3590],[-2221,2981],[1967,2192],[-75,-1583]],[[165121,67753],[789,-1381],[-2196,-2064],[-4990,-31],[652,2234],[-1354,1679],[7099,-437]],[[175726,65330],[-1773,486],[2764,1288],[-991,-1774]],[[167919,65653],[-704,1752],[2341,30],[-1637,-1782]],[[227524,78675],[1407,176],[346,-1810],[1641,1998],[2068,-264],[-1873,-2157],[3305,1133],[94,-2024],[-1323,-990],[-6544,175],[-4965,1629],[-5749,813],[380,889],[5676,880],[698,-1240],[3382,1673],[1457,-881]],[[293460,71648],[-612,-3038],[-3684,1650],[-324,1466],[1780,1570],[2508,-435],[332,-1213]],[[246776,71151],[-1415,3309],[2396,79],[-981,-3388]],[[492964,85456],[1224,-315],[-1920,-2052],[-2094,2867],[2790,-500]],[[295259,86241],[-1384,-1711],[-5476,-1234],[-900,1132],[5678,2100],[2082,-287]],[[331597,134082],[2154,-324],[-1032,-773],[-1122,1097]],[[346762,142020],[-615,875],[1989,-263],[-1374,-612]],[[338949,137923],[941,-643],[-3790,-1122],[890,1194],[1959,571]],[[396935,184408],[2158,-1100],[1466,-3035],[-797,-612],[-1169,2107],[-2754,2105],[1096,535]],[[301693,184601],[1024,-467],[-337,-1554],[-917,846],[-1320,-412],[-728,1500],[594,965],[1684,-878]],[[302801,179654],[1594,-243],[38,-1520],[-1488,614],[-144,1149]],[[306380,179352],[2229,-586],[1717,-1406],[651,-2298],[-3469,2828],[-319,-1650],[-1578,1759],[769,1353]],[[313667,177961],[-2751,-400],[-642,1425],[2933,19],[460,-1044]],[[320696,180552],[2039,-51],[-2284,-1052],[245,1103]],[[341609,129267],[-3451,-2313],[-2179,-2791],[726,-1717],[-1874,1058],[-1395,-508],[-3047,-3257],[-1787,-37],[349,-1175],[-1577,-865],[1081,-1407],[-1525,-1607],[1294,-1653],[2573,1177],[965,-347],[-1286,-2116],[-1121,1137],[-1232,-897],[-2240,348],[-63,-2623],[-1536,2467],[-1375,-83],[-903,-2230],[897,-1298],[-2587,397],[-947,-2416],[-1156,-666],[-545,-4764],[2065,-225],[-1544,-998],[584,-1459],[3231,-1115],[783,1786],[946,-3389],[2793,-3213],[1360,-3174],[-1158,-1450],[2425,-745],[-1267,-2429],[1800,194],[457,-2810],[-2251,-1870],[2070,555],[496,-1517],[-3393,-1171],[4270,-326],[-31,-2093],[1690,-5031],[-2665,-315],[-2575,1070],[1032,-2087],[2106,-663],[-67,-1532],[-2598,-957],[2929,-1237],[-631,-1407],[-3401,220],[853,-2498],[-2168,746],[-3378,-1584],[1927,-885],[-2913,-825],[2545,-940],[-8353,-3329],[-8150,-1998],[-2197,-1800],[-4731,-581],[-9638,1015],[-5338,-287],[2615,-3822],[2394,-1180],[7041,-689],[-1110,-1801],[-4335,-1679],[-8139,1406],[-7943,1116],[-2331,-791],[11134,-3252],[-1212,-1842],[-6731,-459],[-6434,2433],[-3207,-290],[1397,-1855],[4442,-1929],[2136,-2381],[1120,1072],[11082,-29],[1299,-1786],[-1462,-1638],[-8618,-553],[4123,-1006],[4913,426],[3252,-4193],[6131,-680],[5588,1767],[2163,-1407],[15247,-3939],[-3239,-2255],[2769,-2775],[11279,1090],[-5213,-2019],[4265,-3381],[-1082,-1441],[5760,-694],[6007,3662],[9354,3791],[15626,1826],[5841,-325],[229,-3344],[6962,1430],[6210,5761],[7394,2462],[4340,-1077],[5229,2449],[16661,2835],[14135,653],[-165,1725],[-15693,1016],[-1018,2583],[-7440,-388],[-9014,2692],[162,1813],[3037,3740],[2810,2439],[5591,1573],[8693,4678],[7985,2447],[4971,1126],[7877,497],[2629,1133],[6063,360],[-1235,1121],[4029,5380],[4517,-435],[3053,2783],[-3264,-47],[-2139,1786],[2563,3243],[3542,-156],[65,2310],[3025,-308],[4754,2204],[1497,3028],[-3305,1707],[-565,1328],[1912,346],[1926,-1356],[2601,2544],[-182,1210],[2412,-1467],[1623,-2955],[2591,748],[-445,3592],[5382,1348],[968,-853],[-1472,-2780],[9182,30],[2215,-667],[2630,994],[1458,-2647],[3828,3022],[4929,1792],[6954,1448],[6356,954],[766,818],[2351,-695],[1718,1719],[5727,-3230],[1383,-224],[3790,4224],[2103,-1715],[5512,114],[2161,712],[345,-1147],[3932,-847],[855,1483],[2122,-19],[191,-3609],[5015,349],[1762,3465],[1836,-1282],[-367,-993],[2070,-993],[2291,2403],[1200,-261],[1447,-2627],[4839,-755],[6702,2586],[3033,1676],[3821,440],[3451,1334],[1023,2230],[-1172,3259],[1539,2280],[2976,-77],[-1054,-2352],[2173,28],[2114,-3476],[3783,173],[1100,-939],[2860,-80],[1986,-1077],[2349,3438],[441,2716],[3525,2323],[3545,1323],[1141,1354],[5222,1297],[805,800],[3916,899],[446,2070],[2342,-835],[-794,-970],[4253,-1312],[-49,1621],[1636,1741],[-2095,1085],[2172,605],[3627,-1499],[-612,4443],[4311,2516],[4965,954],[3544,-340],[2118,-970],[4098,-3159],[-2888,-76],[682,-2060],[-657,-1724],[2047,1235],[2044,250],[3083,-1276],[1426,-1514],[3420,591],[6127,-1555],[2809,826],[12858,-2259],[3024,869],[1571,-4273],[-1244,-1616],[265,-2930],[-2009,-838],[489,-2924],[-2512,172],[-2528,-2582],[1869,-887],[3000,580],[637,-627],[-1048,-3579],[-2321,-2110],[-1681,-3625],[-2397,-7146],[2091,-539],[1804,1271],[1245,3381],[3153,833],[4783,4447],[1745,5434],[2376,1842],[172,1775],[3111,2091],[4116,-889],[1298,1881],[5351,3002],[2524,4686],[1560,940],[7581,2544],[3576,515],[3280,2894],[3405,-277],[6304,2209],[4773,-206],[3675,1306],[2922,562],[5252,-1077],[2432,1115],[1948,-767],[4332,778],[1719,-638],[1495,828],[1670,-1205],[5640,1853],[1626,2411],[3332,509],[3547,-728],[6766,-2503],[2178,-355],[1706,-1146],[4661,-1449],[3220,2279],[790,2650],[6090,1640],[1697,-769],[1742,-2551],[2704,-1189],[902,-1247],[-1004,-1521],[-3563,-1089],[99,-1358],[7463,2333],[6254,-577],[3917,954],[-3040,-1277],[-410,-1015],[6541,1657],[1931,1371],[5592,1533],[2498,-239],[2139,1638],[2221,-789],[2433,-3279],[2472,-402],[2240,459],[1400,3394],[3363,1643],[2442,-264],[4482,916],[2040,-1159],[307,-1183],[2951,2071],[3338,-1847],[933,588],[3474,-1210],[3060,-179],[1829,-837],[5758,-542],[1985,-1221],[2897,807],[502,-1269],[1973,-300],[-1887,-3867],[686,-625],[2608,1622],[2354,11],[2368,-2017],[738,-2395],[3786,-581],[7255,486],[195,-2248],[3909,205],[1488,-753],[1732,758],[222,2246],[1153,-404],[3668,-3594],[5447,-1684],[1254,758],[5142,-2021],[1183,-2684],[2298,-2028],[1076,-3019],[2395,-842],[-686,2039],[1765,1897],[1872,-1873],[2929,653],[6177,-911],[2618,-864],[1674,-2210],[5528,-2650],[759,1254],[1177,-2665],[-1764,-470],[-490,-3867],[-3692,1281],[3083,-2039],[-788,-1906],[-6637,-574],[1278,-1122],[-1721,-1230],[-3155,-287],[-1426,-1698],[-604,2942],[-1055,-1043],[72,-2782],[1465,-3349],[-4355,178],[-1305,813],[-1837,-2351],[-196,-2037],[-4508,-993],[2758,-413],[2533,-2617],[-146,-5329],[2379,-4958],[2227,-1783],[-1232,-2017],[1807,-543],[2192,1620],[683,-1562],[3875,-1260],[-6732,-503],[-5542,-1744],[-2450,2089],[539,-2873],[-2113,303],[-3595,-5214],[1840,-898],[-5515,-2446],[5469,-9],[-201,-5425],[6531,-3114],[2386,-1902],[-6658,-1793],[9622,802],[10723,-4212],[-1382,-1756],[7520,-437],[3056,-1235],[21397,-3698],[-975899,-1624],[13312,-1639],[11050,-486],[17983,-1689],[-1464,2223],[-18283,1673],[-3840,4281],[-7229,-28],[-4485,2139],[-13607,3256],[6236,312],[9455,-2260],[6113,167],[4921,-1581],[13891,-447],[5600,1164],[-697,1285],[6167,882],[6804,3147],[-4743,3015],[2114,1425],[-8545,2257],[1401,929],[23349,1550],[-6814,3241],[986,1206],[5287,469],[391,1749],[-6429,1364],[-4446,1801],[-8662,1639],[-3498,1952],[6044,2229],[-8259,352],[-3428,2496],[798,3681],[5561,304],[6393,-717],[1248,-1120],[4837,-59],[5559,-2202],[4339,1985],[-1156,2117],[8042,-2268],[-205,4378],[-4416,1840],[-3502,-326],[-2925,758],[3706,1547],[6763,-1896],[-1382,1923],[7801,3176],[3457,432],[4044,-1512],[-969,1186],[4255,1972],[5758,814],[2970,-378],[892,1797],[2406,863],[5241,-957],[3961,511],[6273,-746],[5355,1020],[6971,29],[4067,-346],[11702,700],[2836,1553],[9862,-371],[874,2764],[3562,-593],[-1672,-5291],[6706,1123],[-305,3096],[1738,484],[2426,-1059],[19,-2037],[-3230,-2505],[6086,-305],[7369,-943],[4771,1357],[8870,-72],[1859,-1745],[6893,1374],[-5321,1880],[986,2118],[-3148,174],[-1288,2736],[-2921,829],[990,1585],[3959,-834],[5803,865],[-3124,1236],[-7374,484],[-1943,2973],[2810,349],[-127,-1346],[12213,-273],[5167,-1634],[2544,527],[2751,-550],[5570,797],[3667,-834],[1737,2023],[2651,523],[594,1164],[2591,-981],[-625,-2195],[2829,220],[1008,-959],[3435,959],[2279,-1836],[7771,-2103],[2430,703],[129,2508],[2572,-588],[-294,2783],[2557,-861],[3094,-2761],[4326,565],[-551,-2273],[4877,1217],[3603,-362],[2964,1491],[11411,2029],[3206,1606],[2331,4409],[-1757,3338],[-123,2779],[-1066,3768],[-1431,2382],[-846,3480],[3712,118],[1102,1489],[-1154,1777],[1659,3678],[-991,1278],[1236,2948],[-2172,-119],[-18,2574],[1495,758],[1342,-1529],[-152,2974],[1887,538],[639,3049],[5054,3881],[-667,1785],[772,862],[1948,-628],[-169,1166],[4133,2002],[2133,3147],[3761,1497],[1772,1593],[4118,1929],[1025,-868]],[[291702,91617],[1526,-1313],[-1142,-1361],[-2438,493],[126,1270],[1928,911]],[[574603,87746],[-2366,49],[821,1781],[1545,-1830]],[[305413,94809],[1762,-2334],[3003,-7541],[263,-5724],[-2689,-4219],[-3706,-771],[-5067,-32],[-1997,1551],[1300,788],[4849,-542],[-2847,1455],[2991,1270],[-3995,1571],[-1857,-1661],[-1959,514],[-1008,-1985],[-3770,1728],[168,1561],[2502,-139],[568,1513],[5488,284],[-2358,1232],[3728,-107],[1456,949],[3788,421],[-3386,898],[-32,1334],[2036,1042],[1971,-218],[-1970,1411],[-2202,-217],[-2037,1346],[308,3237],[-875,2499],[4558,1257],[1016,-2371]],[[545062,89959],[-1454,1389],[2573,136],[-1119,-1525]],[[300040,91791],[-2198,317],[141,1374],[2057,-1691]],[[327784,91653],[-1583,3208],[2088,-1160],[-505,-2048]],[[311143,104968],[-2536,-1599],[-503,2021],[2074,3560],[1404,1054],[-383,-2416],[779,-682],[-835,-1938]],[[780503,115614],[-1975,438],[1703,1334],[272,-1772]],[[324498,122769],[956,-605],[-2508,-1556],[-1480,811],[2185,2590],[847,-1240]],[[326873,123038],[-1170,-280],[1161,2714],[751,-739],[-742,-1695]],[[339316,125233],[1657,-257],[-125,-1598],[-1602,-201],[-1462,1576],[1311,1947],[221,-1467]],[[345755,130448],[-2362,-1562],[-19,1204],[2381,358]],[[10618,245580],[-434,-1826],[-1428,1329],[1862,497]],[[23739,421499],[-1273,266],[-371,856],[1226,-132],[418,-990]],[[21295,424942],[302,-2008],[-863,75],[-674,1628],[1235,305]],[[330522,564757],[-2484,-386],[1130,1182],[96,1602],[-474,950],[1994,835],[-322,-1010],[60,-3173]],[[849139,563455],[-543,1210],[478,1904],[65,-3114]],[[789066,566277],[-128,-2142],[-468,2025],[596,117]],[[322640,570662],[-189,-1442],[-1347,557],[1536,885]],[[756952,567249],[-417,1163],[437,867],[-20,-2030]],[[846040,571919],[987,176],[231,-3479],[667,-3091],[-738,614],[108,-1981],[-683,799],[17,3634],[-948,842],[-319,3624],[678,-1138]],[[840266,573530],[1840,-471],[-394,-2483],[-685,-1734],[-1590,-1190],[-581,-975],[-68,2449],[388,4569],[-389,1507],[1479,-1672]],[[847887,578938],[822,-1994],[-83,-3834],[551,-2248],[-1307,-116],[-879,2448],[-91,1286],[-1388,2776],[-251,1928],[2626,-246]],[[273324,548160],[-691,714],[342,994],[349,-1708]],[[465204,548773],[-1181,797],[934,393],[247,-1190]],[[825863,554717],[108,1623],[1486,3123],[639,628],[2525,5965],[731,1306],[-72,1607],[667,2969],[71,-2329],[438,-2379],[-1110,-1777],[-260,-1130],[-1136,-858],[-965,-3911],[-1237,-2245],[-1885,-2592]],[[846093,562701],[-650,-930],[-1178,-37],[-329,1146],[988,1883],[1123,-643],[46,-1419]],[[842029,558417],[-1578,2481],[-421,1250],[167,1586],[1068,743],[-108,2469],[462,2268],[760,636],[863,-1263],[-1126,-5430],[406,-3005],[-493,-1735]],[[842694,560701],[45,3066],[903,3001],[909,4739],[35,-4076],[-275,-1594],[-858,-1756],[-759,-3380]],[[65314,628731],[1381,-1039]],[[66695,627692],[-1247,-825],[-134,1864]],[[63295,630407],[1393,-358]],[[64688,630049],[-410,-585]],[[64278,629464],[-983,943]],[[297147,630270],[-382,-1263],[-1379,-247],[383,1501],[1378,9]],[[61668,631836],[456,-883]],[[62124,630953],[-1320,65]],[[60804,631018],[-453,1580]],[[60351,632598],[863,688]],[[61214,633286],[454,-1450]],[[67829,617353],[-833,346],[-465,4026],[635,1565]],[[67166,623290],[-33,1550]],[[67133,624840],[1759,-1667],[1095,-2784]],[[69987,620389],[-1404,-1566]],[[68583,618823],[-754,-1470]],[[663116,624502],[-224,703],[675,2033],[185,-974],[-636,-1762]],[[833610,576804],[595,-920],[-843,-25],[248,945]],[[329773,595059],[-554,1636],[447,355],[107,-1991]],[[434877,593631],[-948,408],[71,1386],[877,-1794]],[[328918,599549],[-472,327],[31,1737],[544,-500],[-103,-1564]],[[436338,600914],[626,-404],[-355,-1084],[-271,1488]],[[329646,600873],[-541,-14],[141,1651],[400,-1637]],[[430083,605116],[-464,856],[993,22],[-529,-878]],[[433089,603196],[-652,-766],[-151,1094],[803,-328]],[[331038,590589],[-1093,1836],[836,-409],[257,-1427]],[[902059,583271],[-257,1004],[809,839],[-552,-1843]],[[843655,577513],[901,-1899],[14,-1270],[-1429,2631],[-1037,-1605],[218,3897],[1333,-1754]],[[839149,577913],[-474,-140],[616,1904],[-142,-1764]],[[649342,579583],[1178,161],[899,-659],[-1060,-1138],[-1475,-109],[-785,1131],[609,1082],[634,-468]],[[757562,573062],[-524,1998],[447,2022],[451,6775],[570,1110],[32,-1738],[-521,-1836],[288,-2392],[-743,-5939]],[[835289,584576],[1384,-280],[889,-1784],[50,-2921],[-845,-2484],[-874,1735],[-439,2714],[-1007,3242],[842,-222]],[[838651,584936],[555,-457],[-304,-1530],[-488,733],[237,1254]],[[845425,585481],[-874,183],[517,2455],[534,-1224],[-177,-1414]],[[836392,615002],[2067,-1895],[836,1133],[426,-497],[-414,-3828],[328,-2141],[695,-1602],[-1067,-5569],[-1499,-1490],[32,-1561],[-596,-2046],[842,-3478],[-130,-1516],[422,-2178],[1142,-1088],[-33,1291],[808,1032],[1015,-424],[1043,-2982],[247,1862],[1492,-1553],[-855,-911],[748,-2230],[-95,-941],[994,-443],[-230,-2777],[-507,727],[198,1343],[-1771,756],[-411,2356],[-1578,2760],[-353,-124],[575,-3753],[-1673,3171],[-819,884],[-1595,-1762],[-1008,1448],[-567,-475],[-55,2272],[847,1808],[-94,1319],[-846,980],[15,-2358],[-417,-177],[-991,2357],[-529,5845],[-340,1011],[170,1885],[915,-1652],[638,1030],[-234,1823],[287,2526],[-139,4044],[669,5152],[1395,636]],[[285385,614067],[2532,-1810],[386,-1412],[-872,-280],[-912,637],[-978,-1534],[-1564,963],[-848,1859],[-613,159],[215,1363],[954,439],[1700,-384]],[[300613,621536],[615,1050],[1677,110],[2291,-1646],[479,212],[604,-2208],[1151,169],[-829,-1243],[2608,-1262],[960,-1738],[-1061,-2329],[-594,1123],[-2322,211],[-1145,-1136],[-1283,500],[-1064,-373],[-1022,-3715],[-1035,2328]],[[300643,611589],[-810,1122],[-2268,-455],[-1413,589],[-1388,-1240],[-1484,1803],[532,1875],[1767,-831],[2228,-519],[1227,1423],[-1287,2350],[299,2189],[-1925,1289],[774,1452],[1335,-17],[1159,-926],[1224,-157]],[[316307,613993],[1390,-376],[23,-824],[-973,-1588],[-3405,118],[106,2992],[2859,-322]],[[808024,623158],[347,-1993],[-533,-578],[-694,-2287],[-336,-2512],[-2588,-3138],[-2272,1878],[-183,2207],[161,2551],[1619,2505],[107,849],[3787,1378],[585,-860]],[[782522,517031],[-293,-2149],[-423,2086],[716,63]],[[856816,516873],[-656,1455],[1067,1778],[59,-2210],[-470,-1023]],[[767954,518699],[-1820,1751],[-7,1543],[1698,-2375],[129,-919]],[[852267,528613],[-164,3048],[455,-1522],[-291,-1526]],[[524265,526983],[585,-775],[-673,-2393],[-637,243],[725,2925]],[[800877,526576],[-599,88],[-269,2003],[551,936],[531,-1270],[-214,-1757]],[[722171,562852],[753,-97],[1273,-2547],[1838,-5539],[175,-1852],[1217,-4921],[-35,-2293],[-622,-2820],[-715,-1092],[-1822,-1551],[-1270,182],[-477,849],[-655,4006],[-421,7325],[280,-93],[712,6195],[58,2856],[-289,1392]],[[839146,542801],[-790,1099],[694,752],[638,-603],[-542,-1248]],[[836553,540712],[645,-436],[-1280,-627],[635,1063]],[[760805,545187],[-572,2089],[486,138],[86,-2227]],[[850016,559939],[520,-263],[351,-2559],[-495,-1288],[621,-850],[195,-3857],[375,-921],[32,-2545],[-1083,-2341],[-7,-3217],[-1014,6065],[-1176,-3185],[520,-1955],[222,-2886],[-1056,-2052],[-54,2375],[-647,-963],[-2285,2149],[-375,1014],[-257,3491],[615,2386],[-662,1589],[-1321,849],[-283,-2372],[-818,1735],[-704,-1014],[-143,1144],[-816,-294],[-894,-3961],[-589,-213],[466,4990],[570,1291],[1832,1138],[59,1054],[1158,1806],[1152,-1603],[-267,-2218],[1235,1015],[704,2232],[778,-257],[381,2425],[757,-613],[191,938],[803,-73],[-236,3877],[297,533],[1348,-2596]],[[293373,191181],[2597,-1316],[-2276,373],[-321,943]],[[297435,187868],[1994,-1930],[-993,-1578],[-1099,46],[541,1191],[-1503,-469],[-26,1273],[-1476,1087],[1107,804],[1455,-424]],[[309361,192779],[809,-1466],[-416,-2138],[909,-269],[424,-1527],[1984,-2877],[2941,-2866],[2935,-857],[-464,-1184],[-3236,-913],[-1165,634],[-3583,637],[-1203,-214]],[[309296,179739],[-2314,-31],[-659,870],[-2149,-578],[-3916,1864],[3356,866],[-469,2753],[934,1521],[421,-2130],[-694,-110],[1280,-2214],[1186,435],[1358,-1491],[580,893],[-2622,1763],[-446,2062],[2212,1665],[-790,864],[-1260,-497],[-1027,1255],[2672,4235],[920,-1043],[1492,88]],[[291370,215386],[-1109,-2334],[-314,2042],[1423,292]],[[292868,216836],[-988,-204],[-764,3862],[1030,736],[722,-4394]],[[692179,213769],[973,802],[366,-1722],[1815,1223],[654,-847],[-471,-1377],[-1307,505],[-376,-838],[1464,-553],[-645,-741],[-2561,1059],[-1028,-720],[-37,3468],[799,2426],[354,-2685]],[[290249,215820],[-391,1050],[251,3053],[470,303],[647,-3572],[-977,-834]],[[291514,206740],[-1095,-280],[340,1964],[1252,-581],[-497,-1103]],[[293121,213543],[-329,-5088],[-1015,2806],[-294,-1892],[-1344,362],[1288,3087],[-272,1106],[1085,2241],[630,-293],[251,-2329]],[[293575,234020],[-1047,171],[1054,2902],[-7,-3073]],[[295179,241703],[-688,-593],[711,-3699],[-1033,-1222],[-1441,4013],[1446,1532],[547,1508],[458,-1539]],[[297260,239419],[-1010,-314],[-264,1138],[659,1814],[1232,-1269],[-617,-1369]],[[967067,227084],[268,-1231],[-1997,-1118],[728,3310],[1001,-961]],[[911110,269175],[825,-1352],[-531,-1637],[-941,2321],[647,668]],[[899799,267051],[58,3154],[534,-2192],[-592,-962]],[[882212,292985],[1250,-98],[-1663,-1893],[-1925,247],[-323,1684],[1931,926],[730,-866]],[[980875,260159],[1729,1721],[1569,-181],[-648,-2429],[594,-1843],[-2050,-4606],[-900,-2717],[-1351,-2240],[919,-3080],[-2461,-127],[-2048,-1421],[-856,-4987],[-1205,-4186],[211,-1106],[-992,-415],[-2036,-3618],[-1633,-468],[-1990,150],[-536,1439],[-1408,1004],[-2641,-29],[-607,2288],[1326,1658],[-712,-70],[810,2487],[3725,6172],[1222,534],[5375,6307],[1419,3113],[650,3597],[1456,2073],[358,2948],[1392,2542],[967,-1955],[352,-2555]],[[295074,247915],[-1706,677],[494,2092],[480,6418],[1414,-598],[158,-3377],[-884,-707],[980,-2078],[-936,-2427]],[[902895,263078],[668,100],[2874,-2332],[1867,1014],[1292,-55],[1427,1315],[901,-992],[24,-6475],[-244,415],[-804,-3569],[157,-3464],[-542,-373],[-590,2218],[-628,-479],[-1315,-4065],[-904,615],[-1403,-227],[-136,1013],[-1408,2663],[-802,4122],[882,-732],[-1669,4208],[-748,3930],[201,1828],[900,-678]],[[981302,297748],[1597,-542],[1323,-1306],[723,-3193],[-526,70],[1142,-3174],[-222,-3150],[1603,-901],[675,-1233],[-227,4300],[1154,-2856],[449,-3809],[2034,-1712],[1572,-600],[1870,2583],[1464,-813],[-746,-5090],[-810,-1013],[-187,-3065],[-1392,939],[-1260,-1697],[433,-1811],[-744,-2871],[-2387,-6253],[-1869,-2354],[-401,1145],[-1473,758],[1465,3956],[254,1969],[-680,1997],[-2986,2625],[-356,2012],[1644,1226],[554,1052],[327,3314],[591,2495],[-950,4187],[-1103,3587],[591,-649],[-25,2144],[-1320,1314],[77,-934],[-2301,5750],[205,1120],[-1346,3324],[716,-474],[848,-2367]],[[989070,274777],[-712,800],[-39,-1605],[751,805]],[[814396,350367],[-607,1599],[50,1558],[557,-3157]],[[925214,352158],[-109,3303],[566,1604],[301,-833],[-758,-4074]],[[970372,392507],[-965,408],[435,1423],[530,-1831]],[[956117,384770],[648,-214],[2100,-2884],[1308,-2951],[1780,-2193],[1775,-2683],[-468,-1694],[-1659,1700],[-224,785],[-2372,2554],[-874,1396],[-2105,4796],[91,1388]],[[654992,378293],[-1207,389],[-143,2190],[974,-13],[376,-2566]],[[965001,379352],[-1022,1410],[476,1475],[546,-2885]],[[660142,383355],[-745,-111],[7,1630],[752,1414],[377,-1319],[-391,-1614]],[[995222,401798],[865,-1656],[211,-2544],[-1411,-1003],[-868,-28],[-1461,1051],[-159,1268],[983,2383],[1840,529]],[[85218,399912],[-715,-265],[-90,1205],[805,-940]],[[967903,400788],[220,-1515],[-1018,519],[798,996]],[[967490,407932],[-1022,639],[653,871],[369,-1510]],[[999997,408927],[-1056,-2128],[-142,-1301],[1007,1350],[-8,-1333],[-1412,-367],[-603,556],[-1377,-1561],[-581,1115],[2929,3187],[1243,482]],[[967907,405311],[-737,-148],[49,1245],[688,-1097]],[[887521,406531],[-968,-999],[370,1625],[598,-626]],[[292158,198837],[809,-2144],[-889,-1651],[-704,2853],[784,942]],[[336527,200971],[2427,-678],[384,-1926],[-2348,-1344],[85,-965],[-1508,483],[-555,-1720],[-487,2267],[1420,1421],[582,2462]],[[332538,199833],[2828,204],[-1815,-3211],[-2125,-1297],[-764,777],[2009,1690],[-917,2454],[784,-617]],[[937461,461289],[-1069,40],[-1789,3604],[61,629],[1731,-2063],[1066,-2210]],[[816234,462622],[-1038,-670],[-1196,39],[-873,824],[395,1024],[3074,159],[-362,-1376]],[[609712,468136],[213,-1261],[-916,666],[-142,2038],[324,1239],[521,-2682]],[[873713,466550],[-503,-2207],[-824,269],[302,3514],[1025,-1576]],[[359296,503937],[-758,-1160],[-191,1316],[949,-156]],[[840680,473499],[-1007,-298],[240,3272],[925,881],[161,-1868],[-319,-1987]],[[856320,482947],[-826,-571],[106,1008],[720,-437]],[[852391,486423],[1017,-1796],[7,-1434],[-1509,-1129],[-1412,1447],[-421,2423],[2318,489]],[[860429,487739],[1734,-732],[1336,-3442],[-152,-1703],[-2182,2269],[-489,875],[-1044,-747],[-1990,904],[-923,-684],[-798,1532],[-587,-2065],[-118,1682],[890,2109],[3414,447],[909,-445]],[[924903,476538],[-792,1528],[-227,2983],[-1129,2898],[-3795,4698],[-2,829],[3762,-4961],[2325,-4120],[322,-1463],[-464,-2392]],[[823064,481800],[-583,-1101],[-209,2098],[275,2135],[424,524],[93,-3656]],[[933215,465101],[-662,-1042],[-1043,836],[-394,2453],[-1167,1996],[-146,3118],[1012,-1043],[1036,-3108],[989,-1396],[375,-1814]],[[874296,470906],[25,-2747],[-874,-973],[-800,1869],[1286,3441],[363,-1590]],[[929575,472530],[-298,1907],[520,-634],[-222,-1273]],[[911181,470199],[-679,1180],[820,-136],[-141,-1044]],[[869239,469751],[523,3503],[14,-1573],[-537,-1930]],[[610736,475651],[-34,-2071],[-573,-669],[74,2616],[533,124]],[[839007,472500],[-651,1077],[437,1070],[214,-2147]],[[921986,479261],[561,501],[801,-761],[-16,-2314],[-395,-1325],[-679,-290],[360,-2092],[-771,-1232],[-973,73],[-794,-2176],[-2224,-2112],[-2155,-83],[-751,1258],[-710,-295],[-2015,2149],[-157,1304],[1817,357],[683,-523],[1994,742],[229,2446],[170,-2335],[535,-632],[1817,662],[1038,2747],[957,456],[-353,3461],[1031,14]],[[842164,477754],[-552,-3760],[615,-520],[-1083,-2356],[-631,750],[504,1982],[501,4547],[646,-643]],[[917878,488947],[-752,10],[-567,1104],[738,531],[581,-1645]],[[277412,487103],[-256,1303],[874,163],[-618,-1466]],[[908520,493104],[924,-385],[-538,-929],[-1833,-158],[305,1390],[1142,82]],[[778958,485860],[-614,1162],[115,1206],[499,-2368]],[[800575,486958],[-639,-1329],[-331,803],[-677,-729],[143,3810],[1135,-182],[600,-1380],[-231,-993]],[[778344,488478],[-525,-466],[-9,1833],[534,-1367]],[[777341,490836],[-656,507],[221,1150],[435,-1657]],[[850152,490195],[-534,2216],[361,387],[173,-2603]],[[876315,495287],[2021,-375],[1920,-857],[-1845,-557],[-2058,1335],[-38,454]],[[862092,494702],[182,-1665],[-475,-452],[-1418,1072],[1711,1045]],[[850066,494114],[854,-196],[-2497,-684],[245,814],[1398,66]],[[847137,494613],[974,-620],[-2507,-1155],[-103,1882],[1636,-107]],[[855980,494879],[-1643,-402],[-471,495],[707,1851],[1407,-1944]],[[794571,494827],[889,-4713],[1257,-643],[-574,-1908],[153,-1045],[-1856,1463],[-591,3813],[-1814,822],[1257,3056],[901,129],[378,-974]],[[846913,510614],[-1282,-3109],[-1872,-978],[-1356,125],[-508,943],[-3446,-291],[-1156,345],[-1147,-315],[-868,432],[-976,-388],[-616,-1674],[-317,-2150],[234,-2688],[1167,-2307],[416,-1959],[1018,-216],[1349,3264],[1251,-460],[862,1044],[1691,11],[785,1093],[579,-461],[-5,-2107],[-913,781],[-672,-556],[-836,-2261],[-2188,-3051],[-1029,-493],[1389,-2284],[1528,-5150],[-404,-2487],[1734,-2894],[56,-1422],[-2176,-1132],[-113,-1490],[-1347,190],[-283,1057],[365,2893],[-1956,3181],[390,2304],[-178,2943],[-935,15],[-1110,-2282],[507,-3877],[-206,-2242],[160,-3149],[-391,-3133],[419,-2636],[-1329,80],[-651,-686],[-948,1591],[655,5931],[33,2307],[-566,3311],[-1181,-368],[-506,2257],[-80,2320],[857,1671],[638,3278],[-36,3089],[552,2971],[566,1339],[369,-1073],[-340,4582],[929,4627],[738,1722],[541,-982],[1098,2793],[1467,-441],[421,-868],[2347,-295],[1265,-996],[1071,462],[1581,-532],[1186,1090],[1988,4022],[679,-1179],[-958,-3002]],[[842256,497778],[70,-1292],[859,308],[-860,-1420],[-241,1615],[-723,-1675],[49,2398],[846,66]],[[246466,504866],[1312,-4604],[-294,-1117],[-1294,-453],[-241,1287],[937,1426],[-690,1611],[270,1850]],[[775454,494184],[-804,677],[-757,2759],[742,1672],[1106,-4219],[-287,-889]],[[804750,497722],[-785,-359],[134,1516],[651,-1157]],[[876063,500858],[1418,-441],[1338,-2182],[-736,-728],[-754,578],[-1266,2773]],[[863893,496923],[-610,356],[-303,1752],[1113,-50],[-200,-2058]],[[356020,496222],[796,4646],[811,642],[259,-743],[-321,-2103],[-1545,-2442]],[[362142,503359],[1422,389],[1469,-403],[579,-718],[-445,-2655],[-1081,-4037],[-677,412],[-170,-1107],[-758,522],[-829,-1652],[-1952,14],[-765,4607],[383,4370],[1103,926],[1721,-668]],[[249070,500146],[-577,563],[758,1137],[-181,-1700]],[[854352,502828],[105,-1727],[765,-1175],[-1161,15],[-381,2786],[672,101]],[[790205,502734],[322,-781],[-630,-1138],[-295,1157],[603,762]],[[863369,504693],[1288,-863],[-54,-1280],[-700,30],[-1065,1628],[185,-1249],[-1255,516],[161,640],[1440,578]],[[943750,449782],[612,-952],[1066,69],[1290,-2614],[-2681,423],[-644,1535],[357,1539]],[[918134,449344],[722,-423],[293,-1478],[-1271,254],[256,1647]],[[917625,448471],[-654,782],[584,579],[70,-1361]],[[919668,445353],[599,376],[-182,-1410],[-755,605],[-508,2272],[846,-1843]],[[833367,449176],[774,-1617],[591,-155],[913,-2154],[-1093,-1519],[-817,556],[-1511,2527],[-1433,394],[-351,1111],[630,800],[2297,57]],[[923974,451638],[933,-1480],[-685,253],[-248,1227]],[[946525,455461],[690,-1766],[-148,-1107],[595,-965],[453,-3719],[-1245,2564],[-770,4909],[425,84]],[[847805,449005],[-589,-957]],[[847216,448048],[-1585,-3456],[-1583,-1156],[-593,193],[-152,2040],[333,2085],[744,1433]],[[844380,449187],[613,692]],[[844993,449879],[1049,597]],[[846042,450476],[873,1108]],[[846915,451584],[381,648]],[[847296,452232],[420,1251],[1740,923],[2265,193],[963,852],[915,-645],[-1058,-1722],[-1479,-1436],[-2707,-1885],[-550,-758]],[[828450,455439],[2071,-118],[394,-1958],[-1809,-1116],[-277,1078],[-524,-983],[-3135,-1532],[-759,549],[130,2808],[916,979],[1117,-351],[664,-1687],[1191,707],[-1167,1480],[-165,1141],[1006,161],[347,-1158]],[[824001,453685],[-732,-1867],[-1396,611],[561,479],[-44,1822],[947,1382],[924,-1083],[-260,-1344]],[[937191,453094],[-491,880],[464,854],[27,-1734]],[[841063,453697],[-1913,-788],[-1230,-912],[-658,496],[-1054,-714],[-1346,792],[-1782,-330],[25,2443],[1923,1213],[2317,-1999],[1450,726],[823,-1005],[1988,2802],[76,-1050],[-619,-1674]],[[820688,456402],[712,-1494],[-1029,-1234],[-271,-1078],[-1095,2187],[-636,296],[-383,1535],[2702,-212]],[[844234,455707],[-1032,-1744],[-897,215],[1516,2015],[413,-486]],[[845239,455369],[-613,-1316],[-262,1170],[875,146]],[[846042,456487],[1526,-379],[-78,-877],[-1812,-544],[364,1800]],[[938231,455887],[155,-2192],[-726,2027],[-904,-266],[558,1956],[917,-1525]],[[944108,454156],[-2598,2924],[-1351,2939],[769,-353],[1042,-1774],[2038,-2508],[100,-1228]],[[885819,455018],[-923,472],[648,802],[275,-1274]],[[935243,457777],[-352,2068],[689,-882],[-337,-1186]],[[852224,459289],[-914,-1674],[-1010,394],[-861,-596],[492,1903],[661,-257],[1100,799],[532,-569]],[[884820,455700],[-665,-779],[-1690,-39],[-7,886],[895,3677],[800,1203],[1317,285],[610,-1811],[-564,-2151],[-696,-1271]],[[798260,469125],[814,-1233],[1745,-292],[1063,-3113],[4857,-929],[863,2814],[653,217],[506,-1382],[1072,123],[1520,-1452],[1255,-197],[709,-2239],[0,-1469],[1261,-982],[2284,505],[1038,-1556],[-159,-3019],[546,-2159],[-3695,2861],[-1596,-726],[-3247,617],[-2508,922],[-1579,1534],[-2103,1100],[-1501,224],[-804,-770],[-1484,432],[-1757,1495],[-2305,611],[178,1865],[-2877,1613],[843,1923],[226,2018],[574,1198],[2084,-1092],[614,1151],[910,-613]],[[864792,457323],[-664,798],[481,2337],[1065,2120],[-51,-3042],[-831,-2213]],[[890964,489271],[993,-25]],[[891957,489246],[3074,-2798],[1926,-1404],[1677,-655],[1409,-2089],[1283,-246],[1695,-3103],[685,-214],[1201,-2594],[-60,-3432],[1828,-1269],[1753,-1793],[951,-187],[1181,-2160],[121,-2056],[-2018,-351],[-440,-1227],[637,-2662],[1484,-2952],[1118,-1346],[334,-2670],[567,-832],[367,-2116],[1846,-114],[-124,-1990],[758,-1074],[1382,-431],[-589,-858],[313,-1227],[2203,-1447],[-614,-297],[558,-1248],[-909,-811],[-842,459],[-729,1329],[-3808,993],[-1707,683],[-1002,2343],[-1087,1699],[-147,1945],[-743,202],[-1842,5623],[-846,734],[-2097,891],[-304,1009],[-985,381],[-790,-1170],[-909,539],[123,-1601],[-1178,-336],[265,-1183],[-1441,-656],[-1074,231],[1120,-1198],[780,-1939],[-72,-943],[-1998,-2173],[-1160,934],[-3045,-303]],[[892036,450086],[-580,807]],[[891456,450893],[-2749,5829],[-1526,-521],[-1471,261],[645,3305],[-945,1989],[835,302],[-1245,1717],[734,310],[-1183,3050],[-396,2337],[-639,1618],[-15,1249],[-2698,3204],[-1307,626],[-1776,1705],[-2178,475],[-1226,1513],[-132,1425],[-1223,53],[-812,758],[-891,2687],[-1124,-4135],[-778,-193],[-596,2317],[322,905],[-329,1518],[-2167,2999],[721,641],[1373,-645],[1293,2081],[1161,-646],[822,925],[48,1712],[-2665,-1011],[-1819,180],[-789,1492],[-259,2552],[-1768,985],[-827,-187],[726,3374],[1519,898],[901,1479],[1379,565],[2355,-2176],[1394,-108],[757,-3354],[-393,-2432],[139,-2809],[845,-3775],[465,1751],[208,-2351],[392,145],[538,-2513],[1249,-70],[2102,4515],[407,1835],[1259,448],[910,1020],[-131,1094],[1895,2119],[2344,-1824],[3166,-3302],[2314,-577],[347,-956]],[[897718,433893],[-190,-2068],[809,-1993],[514,-4761],[-106,-1763],[577,-3600],[571,-676],[1420,1368],[486,-1543],[1777,-2670],[-45,-3161],[518,-3435],[-89,-2071],[1322,-3935],[622,-3347],[-260,-3778],[836,-1664],[-101,-1703],[512,-1408],[2604,-1773],[1381,-2909],[1902,-1635],[790,-1990],[-559,-587],[1448,-3229],[692,-2687],[394,-4022],[1052,-1736],[282,2288],[1291,-2338],[620,-101],[220,-5225],[1827,-3284],[1116,-1117],[630,-2350],[907,-1214],[551,-2367],[720,-1363],[18,-1520],[680,-1632],[-224,-2013],[91,-5276],[1274,-6198],[80,-3637],[-712,-2583],[-211,-3567],[-672,-3974],[-240,-5163],[-1068,-3619],[-247,-2331],[-1826,-2737],[-1448,-4028],[-32,-2048],[-889,-2194],[-750,-5218],[-341,-216],[-1034,-3669],[-653,-5995],[-76,-4047],[-1762,-1621],[-2878,-169],[-1071,-613],[-1337,-1688],[-1496,-2633],[-1568,-215],[484,-833],[-185,-1808],[-671,1658],[-2115,1957],[-290,1764],[-925,-1559],[445,2426],[-636,1134],[-1376,-1404],[748,-433],[-1565,-1495],[-1563,-2124],[-2575,2187],[-2463,1068],[-836,-546],[-1148,1698],[-1066,288],[-2342,4636],[204,3458],[-859,3350],[-1609,3057],[622,1383],[-1864,-1749],[-937,176],[907,3486],[-59,1545],[-1113,3518],[-1104,-5766],[-2245,-573],[363,1919],[1047,15],[285,4456],[1217,3448],[-221,2242],[390,631],[-560,1605],[-969,-2194],[-569,-2582],[-2241,-2373],[-1500,-3738],[300,-1676],[-975,25],[-1158,2132],[609,-8],[-735,3995],[-824,1661],[-271,1766],[-1361,967],[-558,2467],[372,1186],[-1897,2166],[-942,-5],[-1263,1348],[-1508,-302],[-1371,1842],[-1604,1188],[-1002,-641],[-1814,147],[-3288,-732],[-2440,-2155],[-2078,-1171],[-3896,-195],[-3219,-3470],[-1756,-1461],[-1322,-4189],[-1229,-900],[-1195,578],[-1741,-599],[-249,696],[-1823,282],[-2740,-808],[-1568,-68],[-1121,-2332],[-1542,-661],[-2111,-3003],[-1537,-658],[-2959,651],[-1472,1143],[-724,1593],[-1994,1601],[-40,4387],[521,-759],[927,664],[466,2005],[43,8877],[-1449,5252],[-507,3507],[-98,4636],[-919,3329],[-252,1948],[-1035,2739],[-380,4344],[-885,2960],[-1046,2550],[156,1847],[535,-2681],[753,1339],[-733,1383],[-538,2283],[883,-696],[1048,-3335],[348,617],[-4,2595],[-1510,5181],[-703,3207],[376,4164],[567,1864],[105,2338],[-311,2286],[765,4139],[459,654],[50,-3877],[655,839],[626,2366],[712,1222],[1659,1447],[1541,2733],[1933,2231],[1943,-400],[2202,2049],[1534,671],[981,1581],[1337,-255],[1695,763],[1896,1448],[837,1109],[871,2201],[945,3729],[1465,2606],[-344,406],[-214,3880],[755,2034],[801,1082],[695,2079],[476,-2525],[1169,-3898],[153,3037],[445,832],[-800,2235],[664,1766],[361,-1124],[752,613],[902,-334],[340,1312],[-542,2106],[161,1568],[863,1234],[868,-930],[-622,1668],[650,761],[785,-519],[-491,2400],[1114,1372],[863,-798],[815,3649],[1071,-942],[927,2470],[2137,-2672],[1464,-3298],[-361,-3423],[514,184],[-221,1513],[840,1511],[1613,-571],[438,-1634],[143,1711],[1021,-1590],[6,1711],[588,130],[-417,1503],[-889,1084],[920,2443],[359,2412],[1169,1604],[-255,2042],[1262,3119],[681,-752],[18,1130],[1160,1774],[407,-1239],[2264,538],[439,-646],[608,1540],[123,2288],[-553,933],[-949,-55],[-894,1359],[1253,399],[1166,-1786],[951,312],[445,-1498],[1997,-748],[924,-1041],[1371,137],[1355,-1405],[1957,2345],[-611,-1929],[653,-4],[897,-1668],[26,1789],[751,1031],[1131,-2324],[-1140,-2574],[-212,-2612],[-463,518],[-1019,-986],[173,-2997],[-295,-2031],[-1328,-3585],[348,-1436],[1874,-2387],[239,-987],[1148,-683],[2775,-3245],[1504,-2875],[2125,-1072],[662,-2544],[2188,-2215],[1320,462],[886,1245],[377,2369],[703,2183],[536,3416],[110,2750],[483,3251],[-286,3475],[200,1879],[-339,2105],[481,3189],[-89,1871],[852,832],[-644,2678],[730,2694],[603,5627],[800,1417],[1057,-3552],[99,-3048],[851,-789]],[[858420,409078],[-1074,-97],[-319,-2064],[567,-1663],[189,2004],[637,1820]],[[637606,431064],[1109,-3794],[656,-5734],[171,-4098],[687,-3872],[-760,-3406],[-879,2979],[-674,-648],[174,-3021],[329,-1060],[-177,-3313],[-633,-1291],[-284,-1859],[113,-3269],[-2419,-15161],[-712,-5281],[-1229,-6617],[-973,-8346],[-1057,-5407],[-1247,-2148],[-1583,-477],[-1807,-1972],[-1092,119],[-839,1238],[-1298,640],[-862,1365],[-966,3779],[-115,3649],[211,1258],[-901,3811],[-365,4959],[654,4105],[829,1050],[308,1857],[912,2880],[459,2711],[122,2923],[-583,2094],[-16,1982],[-536,2678],[-169,5314],[1228,4081],[152,2877],[1203,253],[716,1135],[1198,-57],[283,1058],[1754,594],[1529,2868],[418,-1144],[167,1615],[963,2647],[237,-1712],[797,2650],[-106,1037],[617,2426],[123,2158],[599,-730],[1503,2678],[316,1964],[-344,2755],[1169,2318],[920,-2088]],[[965034,409358],[1178,-2096],[-1076,-624],[-694,3969],[592,-1249]],[[963182,416876],[179,-1958],[737,1314],[344,-3259],[-1226,-862],[-641,4627],[607,138]],[[623545,433141],[79,-1630],[-791,1097],[712,533]],[[879762,422936],[499,-2898],[-1552,482],[248,2055],[805,361]],[[620738,434208],[-665,886],[462,2033],[203,-2919]],[[946044,434821],[-1591,1293],[-9,638],[1600,-1931]],[[862386,435524],[408,-814],[-1319,-47],[60,2056],[517,832],[334,-2027]],[[862828,437320],[1124,247],[681,856],[751,-1463],[-222,-895],[-1411,-2006],[-1219,1829],[-398,2387],[694,-955]],[[926487,436727],[-435,-494],[-488,1395],[923,-901]],[[841524,440086],[-287,873],[1460,1700],[130,-1045],[-1303,-1528]],[[949208,443178],[1084,-394],[743,-2189],[-694,-7],[-1626,1529],[-647,2145],[1140,-1084]],[[362655,504051],[-1073,110],[1194,895],[-121,-1005]],[[791051,503675],[-922,-79],[267,1226],[655,-1147]],[[361838,506306],[-280,-1548],[-1390,216],[185,1116],[1485,216]],[[518498,505431],[-531,633],[607,1049],[-76,-1682]],[[359927,505541],[-549,-502],[758,3125],[-209,-2623]],[[770781,513396],[1251,-2909],[-154,-2047],[-534,-192],[-1680,4913],[1117,235]],[[786187,509140],[-1594,847],[265,1426],[1329,-2273]],[[786573,509872],[-1291,1088],[245,662],[1046,-1750]],[[790514,511922],[-28,-2276],[-593,2071],[621,205]],[[784519,510583],[-552,2119],[596,-673],[-44,-1446]],[[784699,513362],[21,-760],[-1169,993],[-101,752],[1249,-985]],[[768033,535698],[974,267],[1957,-406],[1002,-1931],[945,-2757],[164,-1906],[3958,-5390],[2014,-5485],[1195,-1831],[-164,1744],[605,87],[1196,-3342],[856,-425],[1034,-2148],[867,-2841],[1056,-378],[602,-1324],[-1434,-1633],[2019,1648],[562,-85],[855,-2567],[-995,-1414],[7,-2025],[805,-2092],[1777,-899],[430,-4627],[916,-1621],[-491,-1733],[187,-1098],[806,1264],[1545,-797],[1284,-3639],[-397,-1800],[81,-2506],[-277,-1954],[156,-5016],[-197,-3951],[-549,-729],[-748,1481],[-744,-1161],[-1228,1334],[-105,-2276],[-2140,4887],[-2534,3608],[-1060,1887],[-1138,3276],[-1525,2560],[-1278,3432],[-751,2629],[20,1242],[-1025,3763],[-495,2800],[-1245,3038],[-729,2466],[-1219,1477],[-1006,6771],[-645,2414],[-2399,2703],[-305,2893],[-554,762],[-1174,3554],[-1456,1429],[-2639,5599],[-801,3096],[527,2043],[1237,-678],[811,-1304],[997,-385]],[[854812,509742],[412,-95],[768,2870],[1475,1516],[39,-2761],[-1121,-1360],[-106,-849],[974,-1088],[801,-1977],[-2546,1514],[-267,-1027],[251,-3239],[767,-2863],[-576,151],[-985,2750],[48,3140],[-426,1194],[145,2124],[-521,2392],[588,3506],[1124,2105],[-416,-2169],[347,-2969],[-997,-1883],[222,-982]],[[826676,529600],[-104,-224]],[[826572,529376],[-280,-510],[866,-2292],[-1682,-298],[502,-2638],[716,-766],[1268,-4423],[-771,-1724],[809,-1926],[1551,-2268],[961,-1995],[-1250,-999],[-940,360],[-792,1329],[33,-1584],[-494,-602],[-619,-2925],[-165,-3316],[277,-2649],[-895,-918],[-2680,-5089],[413,0],[292,-4300],[-491,-65],[-265,-3584],[-836,-2775],[-3508,-3405],[-466,4698],[-221,-623],[-1011,1202],[-795,-1050],[-516,1543],[-742,-301],[-859,1855],[-174,-1503],[-1030,-1264],[-876,471],[-1286,-1253],[2,2816],[-1264,732],[-1216,-814],[-989,1064],[-754,-556],[-633,6154],[-319,496],[165,2749],[-644,2296],[-1434,1654],[-415,2767],[377,1755],[-869,1922],[-108,2597],[472,4157],[841,2529],[696,622]],[[804524,516729],[987,-1836],[1014,12],[2081,-1888],[471,4377],[-72,1754],[559,-322],[0,1497],[790,1301],[2804,1284],[854,797],[1115,3173],[1328,2978],[360,2071]],[[816815,531927],[345,-11]],[[817160,531916],[1018,792],[1253,1764],[87,-727]],[[819518,533745],[315,0]],[[819833,533745],[652,196],[575,1549],[-452,1297],[513,1127],[536,-398],[949,3515],[990,2323],[708,2699],[454,-1881],[598,1832],[459,-1730],[978,-1204],[-79,-3157],[1074,667],[310,-1131],[1331,-1602],[1746,-1063],[-253,-1849],[-1278,-809],[-1143,147],[-209,-950],[1046,-1933],[-178,-828],[-1360,-665],[-742,518],[-382,-815]],[[190458,968527],[-4765,717],[7618,2063],[2826,-2488],[-5679,-292]],[[232764,969972],[3660,-1102],[-556,-2089],[-5284,-1107]],[[230584,965674],[-3516,3693]],[[227068,969367],[120,2224]],[[227188,971591],[5576,-1619]],[[785790,974253],[-1312,-2479],[3501,1864],[4092,-1962],[463,-1890],[-4426,-1432],[-6987,-393],[-3116,-1285],[-2208,374],[4412,4417],[890,2493],[3018,1337],[1673,-1044]],[[771316,979611],[-62,-2356],[2624,1727],[4069,-1630],[-1727,-5585],[-5233,-46],[-7679,1540],[-1590,1871],[-3189,551],[5324,3564],[7463,364]],[[215066,972034],[2424,1181],[5817,-2936],[-394,-1660]],[[222913,968619],[1625,-2642]],[[224538,965977],[-3944,205]],[[220594,966182],[-1356,1790],[-4603,1051]],[[214635,969023],[-4425,-602],[-1865,1475]],[[208345,969896],[3420,6]],[[211765,969902],[-1076,2786]],[[210689,972688],[-1849,-1082]],[[208840,971606],[-1995,1335],[411,1725]],[[207256,974666],[6871,-549],[939,-2083]],[[244762,985385],[3451,-3194]],[[248213,982191],[8245,-1313]],[[256458,980878],[-687,-1627]],[[255771,979251],[2624,-1204],[-882,-1861]],[[257513,976186],[4576,185]],[[262089,976371],[995,-2388]],[[263084,973983],[-6467,-3153]],[[256617,970830],[-3122,-546]],[[253495,970284],[-137,-2320]],[[253358,967964],[-3461,2456]],[[249897,970420],[1472,-2391]],[[251369,968029],[-6645,199]],[[244724,968228],[-6288,4486]],[[238436,972714],[7831,2173]],[[246267,974887],[-4106,527]],[[242161,975414],[-6338,-948]],[[235823,974466],[-4639,5012]],[[231184,979478],[5909,-516],[-4783,1448]],[[232310,980410],[2276,435]],[[234586,980845],[-1622,1924],[6125,-783]],[[239089,981986],[-4409,1652]],[[234680,983638],[680,964],[7938,1644]],[[243298,986246],[1464,-861]],[[451076,977642],[-3846,679],[4238,522],[-392,-1201]],[[225579,978561],[-137,-1445],[-3477,1075]],[[221965,978191],[1004,1336],[2610,-966]],[[757453,976808],[-4327,1302],[834,855],[6603,-857],[-3110,-1300]],[[658551,980752],[4,-1518],[-3767,60],[3763,1458]],[[375376,991018],[-5243,1567],[980,2038],[4581,-1724],[-318,-1881]],[[558049,980153],[3866,-1188],[5578,1843],[7120,-1187],[938,-1501],[-4326,-2983],[-4704,-1237],[-3006,1289],[-5568,-83],[-3296,1145],[3082,933],[-5720,72],[-1304,998],[3022,1109],[753,2051],[3565,-1261]],[[639661,984167],[3960,-1420],[-7799,-1886],[226,-1224],[-3736,-300],[-2745,1116],[10094,3714]],[[768129,985046],[3624,-1644],[-1824,-3301],[-9173,-1369],[-6524,2065],[4830,2564],[-548,1168],[7598,1730],[2017,-1213]],[[660989,979403],[-2448,2197],[3904,-173],[-1456,-2024]],[[631783,983731],[-3613,-2411],[-3435,975],[7048,1436]],[[672688,983619],[-3102,-2466],[-4852,610],[799,1748],[7155,108]],[[651996,985285],[8265,-1918],[-5505,-918],[-4572,1045],[1812,1791]],[[676038,982821],[-2371,721],[6338,2224],[1765,-1579],[-5732,-1366]],[[662839,984844],[660,-1553],[-4580,1408],[3920,145]],[[660583,987833],[-998,-2432],[-4817,313],[5815,2119]],[[57298,634654],[-1158,649],[1215,1053]],[[57355,636356],[-57,-1702]],[[270661,632517],[-809,-757],[-637,2059],[1022,586],[424,-1888]],[[272673,641945],[1831,-612],[1468,257],[2704,-2133],[1113,-1987],[1637,-242],[2916,-3373],[388,440],[2360,-3479],[2921,-1717],[-130,-1547],[2112,-491],[1026,-1577],[960,-547],[-237,-1259],[-2883,-1105],[-2411,572],[-4324,-795],[449,1343],[1249,1927],[-349,1400],[-2132,424],[-1371,2005],[-405,2736],[-518,612],[-1017,-391],[-2003,1124],[-1636,1901],[-1285,-63],[-2374,873],[-726,1111],[891,468],[-407,1258],[-2318,61],[-1782,-2763],[-1448,-313],[-362,-1345],[-1310,-989],[490,1766],[-97,1805],[879,1701],[2186,1786],[3212,1321],[733,-163]],[[241809,926906],[-91,-2428]],[[241718,924478],[2771,-3358]],[[244489,921120],[20,-1462]],[[244509,919658],[-2531,-2195]],[[241978,917463],[2711,-811],[3769,-159]],[[248458,916493],[-1896,-1297]],[[246562,915196],[2137,-2499]],[[248699,912697],[-293,-2305]],[[248406,910392],[1025,-1287],[1495,4485]],[[250926,913590],[1694,1491],[2820,-2692],[523,-3228],[-1372,126],[420,-3095]],[[255011,906192],[2581,-3447]],[[257592,902745],[2028,1968]],[[259620,904713],[1623,3296]],[[261243,908009],[1207,4132]],[[262450,912141],[1821,1802],[-1457,935]],[[262814,914878],[-79,3659],[3260,-87]],[[265995,918450],[1601,-800]],[[267596,917650],[3587,-343],[-1058,-874]],[[270125,916433],[3962,-2218]],[[274087,914215],[-1748,-1400]],[[272339,912815],[1879,-1341]],[[274218,911474],[-3531,-1249]],[[270687,910225],[1600,-3463]],[[272287,906762],[1962,-2382]],[[274249,904380],[-548,-2311],[-3261,-2857]],[[270440,899212],[-2449,-1296]],[[267991,897916],[-1103,1838],[-2571,2071]],[[264317,901825],[2833,-4376]],[[267150,897449],[-1813,-656]],[[265337,896793],[-2677,2121]],[[262660,898914],[-3309,-35]],[[259351,898879],[1640,-3014],[-3468,-3956],[-1884,-35],[-4943,3478]],[[250696,895352],[-3500,176],[3393,-1357],[2261,-2301]],[[252850,891870],[5407,-890]],[[258257,890980],[-703,-2203]],[[257554,888777],[-2293,-3809]],[[255261,884968],[-1977,-1132]],[[253284,883836],[-3751,-80]],[[249533,883756],[-215,-1996],[-1945,-320],[-2489,650],[3041,-2050]],[[247925,880040],[-345,-2403],[-1605,-840]],[[245975,876797],[-2534,90],[981,-1652]],[[244422,875235],[-1510,36]],[[242912,875271],[66,-2240]],[[242978,873031],[-2928,-1341]],[[240050,871690],[750,-1036]],[[240800,870654],[-2080,-2662]],[[238720,867992],[-21,-1061],[-1607,-4281]],[[237092,862650],[-386,-2742],[-7,-4061]],[[236699,855847],[319,-2357]],[[237018,853490],[1073,-913]],[[238091,852577],[-125,-2480]],[[237966,850097],[767,2741]],[[238733,852838],[2161,-22]],[[240894,852816],[2329,-8777]],[[243223,844039],[-794,-2022]],[[242429,842017],[4484,1823]],[[246913,843840],[1442,-99]],[[248355,843741],[2226,-1441]],[[250581,842300],[2340,-771]],[[252921,841529],[2426,-2274]],[[255347,839255],[1427,-2435]],[[256774,836820],[5235,-2697]],[[262009,834123],[1710,-1869]],[[263719,832254],[3196,172]],[[266915,832426],[3703,-983]],[[270618,831443],[995,-1986],[-552,-2712],[768,-3188]],[[271829,823557],[-331,-5075]],[[271498,818482],[1914,-3518]],[[273412,814964],[61,-774]],[[273473,814190],[2477,-2833]],[[275950,811357],[499,-2672],[1041,-145]],[[277490,808540],[1081,-979]],[[278571,807561],[1044,3024],[828,-973]],[[280443,809612],[381,-1561]],[[280824,808051],[478,1760],[-685,1399]],[[280617,811210],[1350,3072]],[[281967,814282],[-644,2225]],[[281323,816507],[-1082,6455]],[[280241,822962],[469,729]],[[280710,823691],[-2003,5079],[2100,1089],[2829,2104],[1572,1889]],[[285208,833852],[1874,3270]],[[287082,837122],[363,3553]],[[287445,840675],[-148,2809]],[[287297,843484],[-1622,4963]],[[285675,848447],[-3773,3931]],[[281902,852378],[157,1131]],[[282059,853509],[1939,3002]],[[283998,856511],[96,1753]],[[284094,858264],[1096,715]],[[285190,858979],[55,1456]],[[285245,860435],[-1331,3540]],[[283914,863975],[522,1098]],[[284436,865073],[-1607,-36]],[[282829,865037],[1853,4366]],[[284682,869403],[-1203,1219]],[[283479,870622],[-335,3516]],[[283144,874138],[1932,1287]],[[285076,875425],[4321,-1521]],[[289397,873904],[3253,-620]],[[292650,873284],[2822,1440]],[[295472,874724],[3900,-3689]],[[299372,871035],[2231,-3985],[3176,-535],[511,-1116],[1732,774],[-927,-6315]],[[306095,859858],[629,-1599],[-285,-1975]],[[306439,856284],[783,-23]],[[307222,856261],[-366,-2776]],[[306856,853485],[2936,-271]],[[309792,853214],[669,-2514]],[[310461,850700],[1845,-1100]],[[312306,849600],[2672,1987]],[[314978,851587],[2782,3329]],[[317760,854916],[556,4055]],[[318316,858971],[1319,2706]],[[319635,861677],[1421,-477],[-969,-944]],[[320087,860256],[1347,308]],[[321434,860564],[2066,-4332]],[[323500,856232],[94,-1290]],[[323594,854942],[1756,-2623]],[[325350,852319],[-1433,-1304],[2172,261]],[[326089,851276],[-1036,-2388]],[[325053,848888],[1374,360]],[[326427,849248],[1631,-1734]],[[328058,847514],[-192,-1478],[-1352,-888]],[[326514,845148],[1677,-478]],[[328191,844670],[-351,-790]],[[327840,843880],[1788,-1406],[-105,-1954]],[[329523,840520],[-1919,107]],[[327604,840627],[1770,-2004]],[[329374,838623],[-67,-2163],[1715,-223]],[[331022,836237],[1364,-1026]],[[332386,835211],[413,-1800]],[[332799,833411],[-1180,-2492]],[[331619,830919],[2384,1477]],[[334003,832396],[2116,-949]],[[336119,831447],[602,-1843]],[[336721,829604],[2272,222]],[[338993,829826],[1550,-1809]],[[340543,828017],[-2075,-1304]],[[338468,826713],[-1338,-1782]],[[337130,824931],[-3305,-1274]],[[333825,823657],[-1407,-3367]],[[332418,820290],[2798,2237]],[[335216,822527],[1861,1979],[2011,745]],[[339088,825251],[-733,738]],[[338355,825989],[2156,-387]],[[340511,825602],[780,-2198]],[[341291,823404],[-1090,-1138],[544,-774]],[[340745,821492],[1363,1602]],[[342108,823094],[2030,-901]],[[344138,822193],[867,-2225]],[[345005,819968],[-117,-4172]],[[344888,815796],[403,-2191]],[[345291,813605],[-3558,-4029],[-2204,-189],[-2058,-775]],[[337471,808612],[-1820,-3052]],[[335651,805560],[-2541,-3112]],[[333110,802448],[-3360,-312]],[[329750,802136],[-1208,-580]],[[328542,801556],[-541,763]],[[328001,802319],[-2211,408]],[[325790,802727],[-5979,-155]],[[319811,802572],[-1113,263]],[[318698,802835],[-3408,-641]],[[315290,802194],[-1238,-1292],[-1197,-3822]],[[312855,797080],[-1900,-543],[-2425,-2535]],[[308530,794002],[-2069,-3731]],[[306461,790271],[-2296,918],[2015,-1517]],[[306180,789672],[-362,-1575]],[[305818,788097],[-2223,-4102],[-1561,-2038]],[[302034,781957],[-1700,-646]],[[300334,781311],[-3059,-2827]],[[297275,778484],[-1377,-2793]],[[295898,775691],[-1559,-1400]],[[294339,774291],[178,-929]],[[294517,773362],[-2042,-2022]],[[292475,771340],[3197,2496]],[[295672,773836],[1107,3465]],[[296779,777301],[3496,3685],[1777,736]],[[302052,781722],[2060,1637]],[[304112,783359],[4257,7361]],[[308369,790720],[3962,3441]],[[312331,794161],[3840,2117],[1819,314]],[[317990,796592],[1909,-441]],[[319899,796151],[1596,-1599]],[[321495,794552],[-242,-2954]],[[321253,791598],[-2529,-2381],[-1854,993]],[[316870,790210],[-2160,-986]],[[314710,789224],[2375,-660]],[[317085,788564],[508,-1273]],[[317593,787291],[1847,891],[829,-721]],[[320269,787461],[-581,-2111]],[[319688,785350],[-1130,-1585]],[[318558,783765],[1354,-239],[-206,-1024]],[[319706,782502],[1012,-3836],[1737,-442]],[[322455,778224],[-390,-856],[2121,-1596]],[[324186,775772],[1645,-67],[605,-704]],[[326436,775001],[1465,1460]],[[327901,776461],[1287,-1074]],[[329188,775387],[1280,-2341]],[[330468,773046],[-4118,-2655]],[[326350,770391],[-2201,-1192]],[[324149,769199],[-827,242]],[[323322,769441],[-437,-1166]],[[322885,768275],[-662,939],[-2397,-4604]],[[319826,764610],[-2432,-1819]],[[317394,762791],[-1077,1499]],[[316317,764290],[73,3279]],[[316390,767569],[1610,2165]],[[318000,769734],[-380,163]],[[317620,769897],[3683,3252]],[[321303,773149],[-65,-1013]],[[321238,772136],[2739,1343],[-4291,59]],[[319686,773538],[1662,2730]],[[321348,776268],[-4361,-3630]],[[316987,772638],[-2744,-922]],[[314243,771716],[-701,605]],[[313542,772321],[-185,-2926]],[[313357,769395],[-1799,-588]],[[311558,768807],[-605,-1137]],[[310953,767670],[-1094,730]],[[309859,768400],[-227,-1475],[-1192,1038]],[[308440,767963],[-297,-1992]],[[308143,765971],[-2055,-1928]],[[306088,764043],[-436,491],[-2133,-4651]],[[303519,759883],[-7,-2374]],[[303512,757509],[-863,-2003]],[[302649,755506],[855,-606],[622,-2521]],[[304126,752379],[1425,135]],[[305551,752514],[147,-883]],[[305698,751631],[-1968,-846],[-122,1070]],[[303608,751855],[-1299,-1336]],[[302309,750519],[-615,1812],[-149,-2023]],[[301545,750308],[-2282,-960],[-1832,-39]],[[297431,749309],[-1829,-1560],[-1788,-2452],[-41,-899]],[[293773,744398],[789,-757],[-607,-3565],[-1718,-4294]],[[292237,735782],[-2027,2893]],[[290210,738675],[285,1774],[965,1148]],[[291460,741597],[-1426,-2030]],[[290034,739567],[543,-3247]],[[290577,736320],[990,-3492],[-1557,-5167]],[[290010,727661],[-1114,-2176]],[[288896,725485],[939,4088]],[[289835,729573],[-532,105]],[[289303,729678],[-106,2275],[-896,34]],[[288301,731987],[-146,1414]],[[288155,733401],[731,10]],[[288886,733411],[-652,3495]],[[288234,736906],[1008,1891],[-1523,-1693],[-223,-4105]],[[287496,732999],[130,-2125]],[[287626,730874],[-2186,1904]],[[285440,732778],[-200,-582]],[[285240,732196],[2122,-1790]],[[287362,730406],[793,-1190]],[[288155,729216],[2,-3179]],[[288157,726037],[-965,-204]],[[287192,725833],[910,-1599]],[[288102,724234],[-323,-965]],[[287779,723269],[1111,135],[324,-4366]],[[289214,719038],[-2343,-1292]],[[286871,717746],[2650,-342]],[[289521,717404],[-4,-1498]],[[289517,715906],[-1111,-1735]],[[288406,714171],[-996,914]],[[287410,715085],[-1228,-296],[1282,-1114]],[[287464,713675],[-11,-2922]],[[287453,710753],[-1714,-411]],[[285739,710342],[-1714,-2505]],[[284025,707837],[-491,-2046]],[[283534,705791],[-1328,-131],[-1210,-1146]],[[280996,704514],[-1078,-3193]],[[279918,701321],[-2201,-3349]],[[277717,697972],[-947,-706]],[[276770,697266],[-1416,-2791]],[[275354,694475],[-669,-895]],[[274685,693580],[-487,-3641],[-425,-381]],[[273773,689558],[-173,-2774]],[[273600,686784],[707,-5555]],[[274307,681229],[971,-4407]],[[275278,676822],[1044,-3340]],[[276322,673482],[-873,1609],[249,-2232]],[[275698,672859],[1451,-6955]],[[277149,665904],[514,-3782],[-327,-4090]],[[277336,658032],[-578,-3241]],[[276758,654791],[-1026,-1036]],[[275732,653755],[-1039,-109]],[[274693,653646],[-8,1358]],[[274685,655004],[-699,2748],[-974,901]],[[273012,658653],[-419,2677]],[[272593,661330],[-481,693]],[[272112,662023],[-76,2012]],[[272036,664035],[-620,-123]],[[271416,663912],[-960,3873]],[[270456,667785],[653,1842],[-497,729],[-226,-1423]],[[270386,668933],[-507,756]],[[269879,669689],[508,3791]],[[270387,673480],[25,2380]],[[270412,675860],[-1775,3343],[-1122,2809]],[[267515,682012],[-971,1054]],[[266544,683066],[-941,-1164]],[[265603,681902],[-2600,-1346]],[[263003,680556],[-97,1158]],[[262906,681714],[-1117,1726]],[[261789,683440],[-1941,1375]],[[259848,684815],[-3710,-636]],[[256138,684179],[-614,2384]],[[255524,686563],[-345,-1940]],[[255179,684623],[-2138,287]],[[253041,684910],[-1154,-414],[-744,-1062],[-1769,1264],[-522,-1416]],[[248852,683282],[661,-659]],[[249513,682623],[1216,846]],[[250729,683469],[1064,-2083],[-1018,-1191]],[[250775,680195],[576,-1180]],[[251351,679015],[1383,-1287]],[[252734,677728],[-939,-786],[-1454,2298],[-487,-158]],[[249854,679082],[-446,-1934]],[[249408,677148],[-463,1127]],[[248945,678275],[-1031,-974],[-1498,937],[127,998],[-1802,2243]],[[244741,681479],[-1021,-1654]],[[243720,679825],[-1140,239],[-1402,1077],[-1967,184]],[[239211,681325],[250,991]],[[239461,682316],[-849,-1818]],[[238612,680498],[-1885,-726]],[[236727,679772],[101,1198],[-781,-283]],[[236047,680687],[374,-1965]],[[236421,678722],[-1070,-2410]],[[235351,676312],[-1612,-1917]],[[233739,674395],[-2185,406],[609,-1490]],[[232163,673311],[-1703,-2153]],[[230460,671158],[-707,-2508]],[[229753,668650],[-739,-4166],[424,-3382]],[[229438,661102],[711,-2577]],[[230149,658525],[-218,-2228],[-784,-3382],[-447,-3701],[-527,-10459],[611,-6048],[1434,-5857],[1848,-4415],[463,-3117],[1301,-3490],[1776,-319],[1065,-1103],[700,-2013],[992,121],[1769,1394],[1854,226],[486,847],[2045,617],[474,-1453],[748,-83],[718,995],[-448,1572],[1937,2740],[128,2237],[516,1078],[65,3818],[364,2684],[1481,1571],[2614,827],[2076,1195],[2446,-1000],[340,1034],[845,-1184],[-112,-3179],[-990,-2239],[-684,-2400],[98,-1206],[-710,-1549],[730,-318],[-650,-1369],[438,-382],[-978,-6036],[-564,1513],[68,1863],[-733,-2171]],[[254734,614156],[551,-2078],[-485,-3032],[-115,-5789],[-1061,-2281],[-552,-2116]],[[253072,598860],[807,-749],[26,1103],[1016,-1311]],[[254921,597903],[1695,1071],[1975,-874],[1529,124],[1591,1301],[834,-612],[1417,535],[1150,-1113],[828,122],[1392,-3568],[317,877],[1358,-2223]],[[269007,593543],[-402,-1132],[318,-2737],[-624,-2035],[-431,-4006],[-30,-3870],[-489,-979],[205,-2829],[-412,-968],[492,-1297],[-601,-2026],[628,-2268]],[[267661,569396],[538,-2674],[1861,-4718],[596,-550]],[[270656,561454],[536,-878],[352,-2352],[1288,-440],[1182,-1047],[1434,632],[1976,1912],[1528,2298],[2980,-1135],[1171,-1007],[1968,-3424]],[[285071,556013],[1756,-3888],[-495,3387],[1788,2461],[392,1638],[1378,1095],[-113,1655],[650,5220],[1671,2955],[1084,-715],[149,-1326],[949,3409],[2071,-266],[1644,2467],[1241,1049],[387,1774],[1170,1371],[1256,-502],[347,-1712],[-507,-1093]],[[301889,574992],[-1770,-1729],[996,-4999],[-184,-1673],[-1245,-3722],[978,-2843],[206,-1559],[1080,314],[589,1319],[92,2119],[-927,3305],[-439,3051],[208,1099],[3436,2422],[965,423],[189,1349],[-1043,-281],[-261,1548],[785,1729],[482,-1080],[553,-3055],[1108,229],[1124,-514],[1192,-1604],[719,-3959],[745,-123],[2452,821],[2061,128],[1098,-2218],[2008,-1112],[773,166],[1840,2131],[904,594],[-1209,457],[2226,48],[2206,631],[2286,-52],[-1390,-1150],[-1482,-92],[576,-1175],[-96,-1641],[627,711],[540,-2329],[558,1196],[801,-1492],[673,957],[777,-1549],[1436,-1614],[-724,-1573],[-540,-2932],[-1031,-17],[874,-1108],[1397,1077],[1280,217],[896,-471]],[[333284,555367],[2271,-2812],[1593,-3133],[415,-1304],[-379,-4877],[552,2066],[1201,-387],[2201,-4080],[-13,-3251]],[[341125,537589],[625,2633],[2862,-1170],[309,985],[2763,158],[2165,-1069],[-283,-2660]],[[349566,536466],[858,2508],[1091,-1296],[1542,-820],[2337,-4193],[362,-1666],[395,796],[369,-3017]],[[356520,528778],[293,1479],[909,-1288],[465,-4809],[1038,-6348],[611,-2256],[1394,-1005],[162,-2944],[-1099,-1939],[-1450,-3930],[-1296,-1526],[-337,-1822],[-829,-2189],[-51,-1518],[-832,-2254],[-580,216],[-1208,-1121],[1991,-207],[2923,3845],[-63,-1052],[633,-3830],[797,-1504],[1122,1088],[777,-560],[1126,1153],[-583,-5125],[869,4031],[611,514],[780,2026],[687,-748],[37,2776],[930,2417],[1991,656],[1630,-906],[539,-1131],[1106,-360],[1596,-1875],[516,-50],[360,-2140],[702,1487],[1181,-1655],[316,-1818],[-347,-1900],[631,1216],[-794,-5771],[788,1172],[359,2424],[562,248],[-241,-1873],[722,1339],[1327,483],[208,746],[1232,-527],[1909,-1937],[1037,269],[1549,-1123],[2344,832],[1417,-390],[4135,-5071],[1186,-2956],[1174,-2226],[901,-716],[354,-1181],[1622,-1097],[1696,256],[1195,-446],[873,-2590],[687,-4899],[508,-5301],[-81,-4047],[-898,-5683],[-508,-1778],[-1408,-3208],[-1530,-4216],[-1498,-1993],[-1318,-4010],[-768,-3571],[-1531,-4409],[-720,-666],[-534,1971],[-446,-985],[46,-2116],[-706,-2612],[225,-3039],[-142,-3281],[497,-7164],[-679,-5328],[-251,-3272],[170,-2299],[-924,-1697],[-703,-3848],[111,-3780],[-837,-2749],[-1097,-4903],[-1092,-1994],[-717,-3552],[165,-2457],[-374,-972],[-1620,-1334],[-763,-1606],[-172,-2171],[-2544,-118],[-355,1444],[-384,-1587],[-1783,478],[-2142,-859],[187,-1295],[-1060,-636],[-1424,-2495],[-1411,42],[-2486,-2612],[-750,-1522],[-2054,-2987],[-907,-2483],[-554,856],[-804,-1300],[808,-627],[-400,-1295],[-369,-5255],[344,-2921],[-246,-2144],[61,-3067],[-497,-2961],[-1310,-1753],[-1319,-2915],[-798,-2593],[-739,-3702],[-890,-2796],[-1478,-3452],[-2465,-3759],[-66,1686],[1062,330],[1407,2576],[35,1309],[1311,2457],[120,2768],[-1420,-755],[-850,-4078],[-1413,-1962],[-614,-2972],[183,-1672],[-595,-1612],[-863,-4135],[-1995,-3581]],[[351748,304813],[-1152,-3781],[-1065,-1720],[-2038,-1553],[-2141,931],[-1235,-783],[-2048,1370],[-877,1329],[-1829,-148],[-1586,3347],[102,4325],[781,1710],[-330,2500]],[[338330,312340],[84,-2889],[-705,-902],[-340,-3270],[429,-3137],[-369,-611],[672,-2295],[1444,-1250],[1278,-1741],[402,-1881],[-542,-1270],[247,-2511],[1575,-1673],[72,-2517],[-2010,-5292],[-420,-2021],[-1756,-2074],[-4581,-2384],[-3566,-917],[-1362,-35],[-2145,865],[221,-2313],[670,-773],[-216,-2676],[-432,-414],[-389,-2729],[502,-1888],[-413,-1281],[-1567,-1296],[-2261,-240],[-2999,1993],[-779,-396],[319,-4067],[-113,-2387],[1520,-1779],[-164,-865],[1306,125],[-355,1048],[1203,618],[554,-1733],[-268,-2363],[-1217,-332],[-538,1713],[-906,242],[-1046,-1348],[1966,-1244],[-1851,-1924],[-828,-1993],[125,-2481],[-340,-2539],[-796,-1090],[25,-2053],[-1532,255],[-2087,-1733],[-1709,-4223],[-18,-2223],[2184,-3913],[2162,-521],[724,-1488],[-545,-2854],[345,-678],[-1620,-2377],[-1777,-1691],[-1811,-3667],[-274,-3627],[-1316,-1455],[-1140,2086],[683,-2402],[-1438,-1330],[-726,-3621],[476,-2683],[-919,-671],[1232,-918],[1315,-3804]],[[309879,194532],[-2215,896],[-888,-1280],[-3429,-2057],[-558,-5987],[-839,-617],[-2435,1488],[-365,2243],[1570,125],[1627,2293],[-647,481],[-2973,-2904],[-252,-1222],[-1400,1288],[1047,2929],[3235,851],[-3148,452],[-1327,-3229],[-1452,1405],[1123,769],[-2149,402],[-781,3089],[1281,-689],[417,927],[1519,-309],[1800,2170],[-2483,-1815],[-2259,2307],[741,371],[94,1697],[-2552,1590],[-778,2262],[1136,114],[115,1784],[1262,-2471],[13,1734],[-1234,1726],[822,1300],[119,2195],[1109,-116],[-1319,1322],[-43,3594],[585,2171],[-842,-216],[-306,2754],[2708,30],[-295,2006],[-625,-1623],[-1138,-89],[-845,1433],[1379,3080],[-431,2336],[-463,-578],[-1848,1693],[-1420,-61],[2034,2669],[-395,1687],[2525,640],[334,2069],[590,-172],[-712,-4058],[1074,1612],[-322,-3067],[413,474],[188,3084],[-379,1759],[1468,747],[-674,686],[229,1540],[1732,1446],[209,1764],[-1670,1586],[745,3182],[-290,1045],[620,2411],[258,4425],[985,-785],[-192,2682],[-901,428],[428,1477],[-959,686],[-630,-1405],[-1370,228],[-641,3698],[823,6137],[720,1737],[511,3346],[-850,5081],[188,1934],[-547,2025],[167,3022],[1071,128],[272,2835],[676,1765],[697,4767],[1111,2900],[614,5515],[940,3038],[-218,3303],[419,744],[474,3452],[-668,7212],[-21,4971],[747,1110],[235,2923],[-565,4285],[925,3250],[371,3854],[1128,8282],[-186,3230],[745,3623],[-358,3130],[240,5110],[265,1279],[-543,1170],[69,1845],[643,1234],[678,8031],[-304,4548],[136,5452],[-751,8647]],[[304393,396029],[-2552,3929],[-542,2300],[-1608,1728],[-990,1745],[-907,554],[-2864,2735],[-894,1424],[-2659,2965],[-1193,3037],[-1112,1575],[-1229,4565],[535,2060],[-1801,6912],[-890,1708],[-188,2352],[-1147,2225],[-286,2672],[-1248,4430],[-1602,8720],[-695,2411],[-1014,2220],[-1068,4555],[-968,2471],[-2866,3512],[140,1448],[581,316],[-792,3507],[164,825],[-633,2123],[148,2057],[1346,3503],[1317,2033]],[[276876,484646],[1119,1764],[533,3027],[-769,1335],[-772,-2091],[-1884,3066],[535,667],[-87,4107],[-280,1804],[968,1368],[199,2841],[969,2146],[301,2467],[-176,2219],[964,1156],[2338,1341],[111,1476]],[[280945,513339],[-460,998],[645,1333],[601,-444],[-112,3158],[1381,1074],[1250,2315],[1647,6128],[-973,872],[391,3918],[-320,4114],[-369,716],[792,1440],[-611,2350],[304,1942],[-1503,4294]],[[283608,547547],[-747,1863],[-699,3064],[855,1888],[-2702,3658],[-986,53],[-683,-919],[-175,-1513],[-1718,-1818],[-248,-1254],[1241,-3418],[-1188,-1334],[-1129,-325],[-868,3758],[-170,-1383],[-792,594],[-620,2467],[-1411,1027],[-1234,65],[-555,-1489]],[[269779,552531],[-786,3066],[-854,703],[495,-1782],[-1229,1235],[269,2494],[-717,1428],[-2121,2193],[-156,1498],[-1522,2116],[1046,-2581],[-633,-1417],[-556,1358],[-862,542],[-572,2936],[454,2055],[-669,904],[454,975]],[[261820,570254],[-601,1595],[-1412,2411],[-795,2479],[-2533,4425],[917,448]],[[257396,581612],[-422,2214],[-903,274]],[[256071,584100],[-324,-1295],[-2600,608],[-1141,1154],[-1462,486],[-809,1045]],[[249735,586098],[-1421,1141],[-1498,-20],[-1869,1793],[-1156,1879]],[[243791,590891],[-1594,3514],[-3075,5421],[-2071,878],[-664,1278],[-1566,-2624],[-2081,-1668],[-826,-244],[-1873,1525],[-1582,341],[-1067,1419],[-1060,583],[-672,1363],[-2579,1095],[-927,1190],[-2287,1659],[-2090,2672],[-686,1604],[-2368,833],[-2062,1555],[-1307,2980],[-2850,2850],[-1510,3949],[-520,2427],[1136,1146],[-645,1170],[710,2030],[78,2201],[-618,755],[-605,2191],[9,2008],[-405,1781],[-1696,3365],[-1896,4861],[-1229,2038],[43,765],[-1221,745],[-712,2132],[478,359],[-1987,2304],[-772,333],[395,1499],[-861,-835],[-523,797],[-113,1810],[627,1615],[-786,2400],[-756,-44],[-525,2230],[-1483,1442],[-383,1962],[238,1246],[-1643,609],[-973,2471],[-579,513],[-1338,3248],[-171,1485],[-1430,4241],[-1034,4787],[177,2286],[-1603,987],[-900,1680],[-560,-723],[-2178,2330],[399,-1502],[-257,-2907],[691,-3848],[-45,-1593],[770,-2416],[1714,-2742],[710,-2611],[818,-758],[310,-1700],[620,-519],[380,-3544],[1124,-1793],[904,-2632],[392,-2373],[187,1192],[627,-1020],[659,-3449],[112,-1989],[716,-1557],[991,-4374],[51,-2649],[811,-1428],[290,1446],[667,-1007],[1672,-4114],[-103,-1572],[-1293,-1949],[-452,709],[-769,3551],[-2934,4290],[-1815,3028],[47,3840],[-893,4299],[-1788,2188],[-375,2151],[-1230,-1333],[-673,1453],[-1679,1491],[-263,1261],[-1379,2434],[1296,-343],[748,527],[700,3278],[-269,1062],[-2357,4615],[-1888,2203],[-394,3242],[-501,657],[-184,2309],[-1667,4507],[115,1695],[-631,867],[-778,3175]],[[174644,697459],[-943,4516],[-1703,2527],[-916,129]],[[171082,704631],[-267,1620],[-1770,561]],[[169045,706812],[-1284,1813]],[[167761,708625],[-2431,318]],[[165330,708943],[-454,642]],[[164876,709585],[31,2941]],[[164907,712526],[-630,1712]],[[164277,714238],[-2825,5721],[-10,3601]],[[161442,723560],[-1429,1591]],[[160013,725151],[-330,3344],[1232,-1740]],[[160915,726755],[-875,2858],[1857,435]],[[161897,730048],[-2130,443],[-104,-1673]],[[159663,728818],[-1141,1357],[-525,2333]],[[157997,732508],[-1611,2713],[-511,5649],[-1220,2318]],[[154655,743188],[-132,1417]],[[154523,744605],[663,2836]],[[155186,747441],[179,2455]],[[155365,749896],[-796,4376]],[[154569,754272],[-513,4088],[1086,5207]],[[155142,763567],[249,6434]],[[155391,770001],[361,4735]],[[155752,774736],[49,3585]],[[155801,778321],[1918,-169]],[[157719,778152],[-677,696]],[[157042,778848],[-1689,49]],[[155353,778897],[-108,4478]],[[155245,783375],[-734,3693]],[[154511,787068],[-681,1455]],[[153830,788523],[-247,2821]],[[153583,791344],[2040,-1255]],[[155623,790089],[2642,-516]],[[158265,789573],[683,333]],[[158948,789906],[557,-5003]],[[159505,784903],[623,465],[-108,2659],[419,1127],[-1186,2693]],[[159253,791847],[344,1760]],[[159597,793607],[-677,1367]],[[158920,794974],[-724,0]],[[158196,794974],[-795,2762]],[[157401,797736],[-1545,209]],[[155856,797945],[-35,2883]],[[155821,800828],[-659,-1117]],[[155162,799711],[-753,-87],[-1027,1435]],[[153382,801059],[-768,2925]],[[152614,803984],[-3864,212],[1534,798]],[[150284,804994],[-1721,237]],[[148563,805231],[-319,1130]],[[148244,806361],[-1182,-282]],[[147062,806079],[-1807,1681]],[[145255,807760],[176,1939]],[[145431,809699],[-623,1758]],[[144808,811457],[214,3046]],[[145022,814503],[-863,-2968]],[[144159,811535],[-708,2195]],[[143451,813730],[699,4431]],[[144150,818161],[-721,-480]],[[143429,817681],[-797,2477],[-1191,731]],[[141441,820889],[-93,1622],[1590,-1307]],[[142938,821204],[-1159,2494]],[[141779,823698],[-903,-2657]],[[140876,821041],[-776,-838],[-2144,2799]],[[137956,823002],[811,2426],[-1073,1704],[2414,6170]],[[140108,833302],[-1178,-614]],[[138930,832688],[-111,3136]],[[138819,835824],[-33,-3497]],[[138786,832327],[-492,-1612]],[[138294,830715],[-1003,-1519]],[[137291,829196],[-763,226],[-550,2074]],[[135978,831496],[591,1033]],[[136569,832529],[-848,3942]],[[135721,836471],[-1788,-716]],[[133933,835755],[-554,-2023]],[[133379,833732],[-667,1102],[1054,2601]],[[133766,837435],[-2970,5257]],[[130796,842692],[-1535,739]],[[129261,843431],[-245,3098]],[[129016,846529],[-1818,3185],[-1265,901]],[[125933,850615],[-1300,2714],[-643,3415],[-387,-1286],[1259,-5305]],[[124862,850153],[-2288,517],[-760,2339]],[[121814,853009],[-2340,1455]],[[119474,854464],[2577,-3446],[-1447,-1231]],[[120604,849787],[-2671,1992]],[[117933,851779],[-2246,2998]],[[115687,854777],[-4019,2719]],[[111668,857496],[796,900],[-18,1888],[-1937,-1719]],[[110509,858565],[-1741,131]],[[108768,858696],[-2296,1310]],[[106472,860006],[-3544,753]],[[102928,860759],[-3338,-478],[-2094,1888]],[[97496,862169],[584,1979]],[[98080,864148],[-1548,-1712],[-1808,581]],[[94724,863017],[-2054,2483]],[[92670,865500],[155,1366]],[[92825,866866],[-2243,-1243],[-1708,299],[704,1484]],[[89578,867406],[-2237,-2466],[1647,-1883]],[[88988,863057],[-1297,-2937]],[[87691,860120],[-2679,691]],[[85012,860811],[-563,-1987]],[[84449,858824],[-2732,-1219]],[[81717,857605],[-980,-1869]],[[80737,855736],[-2232,-359],[-311,1290],[1251,652],[-1261,1574]],[[78184,858893],[1116,2491]],[[79300,861384],[265,3083],[2543,1781],[2357,-176]],[[84465,866072],[-980,1780],[-1852,41],[-3117,-2313]],[[78516,865580],[-46,-923],[-2510,-3061]],[[75960,861596],[-292,-1880]],[[75668,859716],[-1255,-464]],[[74413,859252],[-2436,-2840]],[[71977,856412],[-250,-1231]],[[71727,855181],[2343,-1763]],[[74070,853418],[-1904,-2162]],[[72166,851256],[-630,-1976]],[[71536,849280],[-2112,-849],[-2141,-2654]],[[67283,845777],[-1945,-1424]],[[65338,844353],[-420,-1884]],[[64918,842469],[-3444,-2160],[-1857,-1836]],[[59617,838473],[-700,-2064]],[[58917,836409],[-2649,-848]],[[56268,835561],[-2100,-1816],[-2726,-826]],[[51442,832919],[60,1370]],[[51502,834289],[-1708,-2902]],[[49794,831387],[-1299,613],[-369,-1458],[-1835,-933]],[[46291,829609],[156,1675]],[[46447,831284],[1035,437]],[[47482,831721],[2081,3103]],[[49563,834824],[2616,1788]],[[52179,836612],[2565,-1280]],[[54744,835332],[-686,1192],[658,1823]],[[54716,838347],[3644,3235]],[[58360,841582],[3480,4076]],[[61840,845658],[594,5174]],[[62434,850832],[1060,2703]],[[63494,853535],[-2444,-1407]],[[61050,852128],[-2094,1554]],[[58956,853682],[-36,-2735]],[[58920,850947],[-817,171],[-1633,2615]],[[56470,853733],[-694,-540]],[[55776,853193],[-1229,1370]],[[54547,854563],[-2774,-2261]],[[51773,852302],[-2177,-150]],[[49596,852152],[1169,889],[-831,2901]],[[49934,855942],[541,1805],[-1646,4120]],[[48829,861867],[788,2379],[-1521,-2468],[319,-1655],[-3712,-1083],[-2099,2944],[-1920,1407],[1524,2078]],[[42208,865469],[2985,-1789]],[[45193,863680],[860,991]],[[46053,864671],[-4875,1234]],[[41178,865905],[833,718]],[[42011,866623],[-2265,1262],[-1324,2079]],[[38422,869964],[1541,1295]],[[39963,871259],[-262,1369]],[[39701,872628],[1425,2211],[1153,45]],[[42279,874884],[-183,1894]],[[42096,876778],[2048,2730],[2081,-1279]],[[46225,878229],[2049,1303],[939,1561]],[[49213,881093],[3288,170],[891,1546]],[[53392,882809],[-581,2562]],[[52811,885371],[-1186,1629]],[[51625,887000],[1341,313]],[[52966,887313],[-707,2043],[-1591,-638]],[[50668,888718],[-2910,-2619]],[[47758,886099],[-2518,1268]],[[45240,887367],[-3294,-756]],[[41946,886611],[-3455,724]],[[38491,887335],[-757,2036]],[[37734,889371],[-1425,1365],[2031,880]],[[38340,891616],[-3351,690],[-1901,1397]],[[33088,893703],[2816,1300]],[[35904,895003],[4514,3156]],[[40418,898159],[4783,1224]],[[45201,899383],[-850,-2375]],[[44351,897008],[940,-780],[5219,-179]],[[50510,896049],[1003,1348],[-1036,531]],[[50477,897928],[-2165,3101]],[[48312,901029],[1639,-653]],[[49951,900376],[300,-1330]],[[50251,899046],[3496,-1106]],[[53747,897940],[1079,1182]],[[54826,899122],[-1672,583],[-1483,-705]],[[51671,899000],[-1273,880]],[[50398,899880],[651,1653]],[[51049,901533],[-3832,284]],[[47217,901817],[-1997,997]],[[45220,902814],[-1123,2436],[-3503,2600]],[[40594,907850],[-3890,1860],[1128,389],[476,2725]],[[38308,912824],[5296,304],[3170,2675],[580,2193],[2441,2391],[2993,846]],[[52788,921233],[4671,3400],[3655,-196],[4246,3331],[6320,-3593]],[[71680,924175],[2673,779],[2778,-724]],[[77131,924230],[-661,-929]],[[76470,923301],[3461,-1391]],[[79931,921910],[5432,486],[4343,-1680],[5230,-340],[1738,-896]],[[96674,919480],[5497,638],[5029,-2744]],[[107200,917374],[1127,-14]],[[108327,917360],[5056,-801],[2927,-2154]],[[116310,914405],[7684,-2700]],[[123994,911705],[-1429,1308],[514,2335]],[[123079,915348],[4976,1286],[-762,-1632]],[[127293,915002],[2808,1073]],[[130101,916075],[898,1283]],[[130999,917358],[4733,1519]],[[135732,918877],[1772,1400]],[[137504,920277],[2361,-862]],[[139865,919415],[-3643,-2166],[-2508,-490]],[[133714,916759],[-4211,-3927]],[[129503,912832],[1868,-424],[-35,1565]],[[131336,913973],[2585,2091],[2153,-21]],[[136074,916043],[118,-1301],[1714,2648],[3456,1339]],[[141362,918729],[384,-1004]],[[141746,917725],[3576,3246],[-853,1857]],[[144469,922828],[2368,-1982]],[[146837,920846],[1462,-3015],[3403,-2258]],[[151702,915573],[516,2842]],[[152218,918415],[2103,1669],[888,-2492]],[[155209,917592],[-837,-1840]],[[154372,915752],[2268,-13]],[[156640,915739],[1622,2564]],[[158262,918303],[4151,-203]],[[162413,918100],[3441,-2104],[3955,-969]],[[169809,915027],[2149,-1268],[5654,-1220],[1190,803]],[[178802,913342],[3382,-1855],[1248,-1543]],[[183432,909944],[-2225,-763],[-1858,-2181]],[[179349,907000],[4868,-1198]],[[184217,905802],[3463,-90]],[[187680,905712],[4527,874],[1954,948]],[[194161,907534],[1310,-1538]],[[195471,905996],[2882,-840]],[[198353,905156],[1843,-2750]],[[200196,902406],[-1574,-204]],[[198622,902202],[2182,-2087],[233,1559]],[[201037,901674],[1306,-719]],[[202343,900955],[-2266,5037]],[[200077,905992],[587,1778],[3713,998]],[[204377,908768],[1785,1931]],[[206162,910699],[-2297,-1002]],[[203865,909697],[-2809,-156]],[[201056,909541],[-318,-933]],[[200738,908608],[-2645,614]],[[198093,909222],[1036,1975]],[[199129,911197],[5970,1833],[1735,-1193]],[[206834,911837],[309,-1542]],[[207143,910295],[3430,-2530]],[[210573,907765],[1999,496]],[[212572,908261],[2172,-1797],[3159,-703]],[[217903,905761],[3052,868]],[[220955,906629],[3637,-686],[2041,494]],[[226633,906437],[2659,-1127]],[[229292,905310],[690,1410]],[[229982,906720],[-2511,285],[-1500,2729]],[[225971,909734],[3444,788],[1904,-2579]],[[231319,907943],[2097,1113]],[[233416,909056],[-1115,-4119]],[[232301,904937],[639,-1671]],[[232940,903266],[1171,265]],[[234111,903531],[1016,-1990]],[[235127,901541],[265,1670]],[[235392,903211],[-1089,2813]],[[234303,906024],[528,1682]],[[234831,907706],[1666,121],[3381,3503]],[[239878,911330],[-2552,1651]],[[237326,912981],[1336,1328]],[[238662,914309],[-527,1892],[-4941,2227]],[[233194,918428],[-1376,2940],[1852,1313]],[[233670,922681],[-1862,1539],[398,2754]],[[232206,926974],[2336,374],[-854,1401]],[[233688,928749],[1864,1958]],[[235552,930707],[1789,446]],[[237341,931153],[4468,-4247]],[[300051,207169],[-2273,162],[-1078,1268],[-288,-1355],[935,-2423],[-49,1561],[2753,787]],[[299149,209912],[125,889],[-1858,1013],[-276,-1382],[2009,-520]],[[302316,229071],[-1891,1251],[-1754,-1661],[-682,497],[-140,-2140],[1434,1704],[1323,512],[1710,-163]],[[309232,235614],[-815,831],[338,-2143],[477,1312]],[[297313,260123],[1023,603],[-694,1240],[-329,-1843]],[[352392,311289],[-108,-176]],[[352284,311113],[-1241,-2707],[224,-2392]],[[351267,306014],[47,-79]],[[351314,305935],[328,2335],[1093,2133],[674,-457],[498,2232],[-353,1780],[-1162,-2669]],[[327139,322385],[-161,1110],[-989,404],[-932,-1013],[701,-1336],[1381,835]],[[349258,362140],[-575,-4635],[-661,-1153],[536,-871],[-254,-1439]],[[348304,354042],[1138,1142],[-491,355],[329,3186],[-22,3415]],[[357177,383706],[1398,630],[-63,1361],[-1365,-992],[30,-999]],[[375077,393444],[-787,942],[-393,-2275],[1180,1333]],[[307956,408551],[363,-118]],[[308319,408433],[-123,-1621]],[[308196,406812],[119,-148]],[[308315,406664],[1162,1474],[-670,661],[92,1293],[-1567,2729]],[[307332,412821],[-125,120]],[[307207,412941],[-782,1344],[-698,-797],[-202,-2544],[1566,-1193],[-100,-903],[965,-297]],[[386332,449873],[-955,-620],[1,-1374],[-2042,457],[-987,-1134],[-18,-1639],[-573,-510],[-130,-2422],[-562,101],[197,-1444],[-1153,-2421],[1101,-162],[107,1778],[988,2462],[591,-1433],[-573,2822],[1189,2400],[2077,501],[251,1197],[713,268],[-222,1173]],[[361918,478518],[-11,-1010],[1199,-4197],[-183,2487],[-773,6122],[-664,-2804],[432,-598]],[[347518,533051],[241,742],[-889,541],[-578,-2791],[1280,431],[-54,1077]],[[264404,571429],[-1236,3095],[-1495,1921],[-282,-2397],[1061,-2885],[1724,-1068],[228,1334]],[[260823,577661],[-529,957],[-819,-406],[762,-1350],[586,799]],[[253117,597801],[-1417,-1326],[859,-263],[558,1589]],[[214693,624354],[-509,739],[-1329,-346],[1629,-807],[209,414]],[[229370,654923],[-756,-1152],[-164,-7372],[197,3327],[723,5197]],[[223732,665013],[790,-2918]],[[224522,662095],[147,1097],[-937,1821]],[[276031,664008],[-361,1749],[-824,-1134],[869,-1739],[316,1124]],[[178856,701850],[-936,1451],[-314,-514],[758,-1695],[492,758]],[[256497,710969],[1819,-777],[-1046,1001],[-773,-224]],[[184345,716995],[-1002,2446],[-638,-1149],[-571,1110],[-1069,-685],[1689,-686],[611,942],[980,-1978]],[[255397,721963],[-173,-1865],[581,-993],[-408,2858]],[[193423,729241],[-1653,-4426],[1470,3320],[183,1106]],[[311702,775814],[-211,-115]],[[311491,775699],[1149,-785]],[[312640,774914],[-32,205]],[[312608,775119],[-906,695]],[[276535,778914],[1280,-720],[1530,442],[-605,728],[-2205,-450]],[[287558,783985],[198,1824],[-1143,423],[945,-2247]],[[285352,786669],[38,-1448],[902,834],[-940,614]],[[305332,797621],[505,623],[-1163,1887],[-1107,-2007],[556,-676],[646,1365],[563,-1192]],[[255471,800710],[-1691,1871],[-706,-1305],[-186,-3291],[694,508],[1473,-716],[416,2933]],[[204952,786635],[-300,2367],[-1680,-1540],[-2177,-222],[2364,-334],[1329,1284],[464,-1555]],[[216258,786530],[2076,-365],[547,822],[-2660,-124],[-373,1705],[-374,-2070],[784,32]],[[292969,791821],[1197,-526],[-1333,1584],[62,966],[-2380,-2160],[2023,-479],[431,615]],[[299848,791779],[775,925],[-1541,348],[766,-1273]],[[241895,791656],[1342,-252],[-997,1806],[-1634,-469],[1900,-587],[-611,-498]],[[277277,793902],[869,-1058],[1584,-150],[-1498,1579],[-955,-371]],[[236909,794206],[1774,1729],[-1590,1716],[1090,153],[-791,2728],[-137,-1229],[-1568,-600],[125,-942],[1165,961],[-1591,-2895],[-35,-1486],[1558,-135]],[[289481,768262],[-51,62]],[[289430,768324],[-700,-759]],[[288730,767565],[-2596,-1733],[313,-777]],[[286447,765055],[-914,-398]],[[285533,764657],[-1043,963]],[[284490,765620],[-4103,-1205]],[[280387,764415],[-1412,-1521],[-570,-1532]],[[278405,761362],[1213,-722]],[[279618,760640],[345,264]],[[279963,760904],[445,254]],[[280408,761158],[2439,649]],[[282847,761807],[1794,-755],[1535,60]],[[286176,761112],[2069,1618],[-157,1767]],[[288088,764497],[604,887]],[[288692,765384],[-782,637]],[[287910,766021],[1571,2241]],[[296185,766268],[405,1760],[224,3722],[-656,-811],[27,-4671]],[[279310,767227],[793,829],[-583,1473],[-210,-2302]],[[221420,771418],[-171,4958],[-770,-1223],[788,-1524],[153,-2211]],[[188961,747632],[-2295,3939],[-448,-1716],[1643,-4008],[1100,1785]],[[269232,755176],[1352,232]],[[270584,755408],[-154,1442]],[[270430,756850],[-395,631]],[[270035,757481],[-1858,-5463],[2711,-2040]],[[270888,749978],[2103,669]],[[272991,750647],[1374,1537]],[[274365,752184],[2483,1602],[3126,3047],[921,1445]],[[280895,758278],[-523,1818]],[[280372,760096],[25,-1444]],[[280397,758652],[-3232,-520]],[[277165,758132],[-641,-953],[-2648,97],[-1340,-2209],[-1992,-1373],[-1399,271],[87,1211]],[[264522,776026],[3593,-2701]],[[268115,773325],[515,-1768],[-3,-2116]],[[268627,769441],[-101,-1826]],[[268526,767615],[-1523,-2444]],[[267003,765171],[399,-1990]],[[267402,763181],[979,1761]],[[268381,764942],[1210,848]],[[269591,765790],[788,-1261]],[[270379,764529],[684,-4957]],[[271063,759572],[1758,1833],[212,4762]],[[273033,766167],[1140,2761],[54,1145],[-1164,2733],[1113,-15]],[[274176,772791],[103,-1306]],[[274279,771485],[1011,-1465]],[[275290,770020],[1860,-1609]],[[277150,768411],[327,1762]],[[277477,770173],[1086,-270],[-1024,2062]],[[277539,771965],[124,1381]],[[277663,773346],[-857,337]],[[276806,773683],[-1300,3269],[-2164,92]],[[273342,777044],[-52,905]],[[273290,777949],[-3917,367],[-2962,1064]],[[266411,779380],[-204,1195]],[[266207,780575],[-931,-470]],[[265276,780105],[321,2503]],[[265597,782608],[-1074,776]],[[264523,783384],[492,1679],[-1166,1888],[443,1843]],[[264292,788794],[-2565,120],[-905,1422],[-812,3086]],[[260010,793422],[-2298,265]],[[257712,793687],[-2889,1172]],[[254823,794859],[85,-2231]],[[254908,792628],[-749,1398]],[[254159,794026],[-755,-1481]],[[253404,792545],[-1184,-783]],[[252220,791762],[-963,-2595]],[[251257,789167],[-159,-126]],[[251098,789041],[-3757,-2772]],[[247341,786269],[-3076,-4176],[1571,-312]],[[245836,781781],[1536,1111]],[[247372,782892],[581,-1569]],[[247953,781323],[744,-620],[3094,1764],[1957,2074]],[[253748,784541],[560,-2551]],[[254308,781990],[647,889]],[[254955,782879],[1606,-740],[777,-1790],[2002,-423],[1404,1385]],[[260744,781311],[3234,497]],[[263978,781808],[-185,-1570]],[[263793,780238],[1964,-86]],[[265757,780152],[279,-1823]],[[266036,778329],[689,-1315],[-1813,540],[-473,-1056]],[[264439,776498],[-1622,1388],[-3271,-1340]],[[259546,776546],[-1141,-944]],[[258405,775602],[-23,1149]],[[258382,776751],[-2355,-5774],[-386,-2314]],[[255641,768663],[1013,1747]],[[256654,770410],[742,-421]],[[257396,769989],[-1568,-9070],[335,-2911],[-45,-3211]],[[256118,754797],[1069,-3288],[943,77],[1224,1433]],[[259354,753019],[925,2999]],[[260279,756018],[215,2861]],[[260494,758879],[-889,4365]],[[259605,763244],[64,2586]],[[259669,765830],[620,1522]],[[260289,767352],[163,2308],[1724,2799],[187,-2494],[482,2993]],[[262845,772958],[1135,685]],[[263980,773643],[-40,2219]],[[263940,775862],[582,164]],[[304140,818688],[-1150,-344],[1394,-1772],[-244,2116]],[[65144,846949],[-1377,-1292],[582,-619],[795,1911]],[[66775,847565],[-2377,1168],[-43,-804],[2420,-364]],[[204838,846536],[-2252,-1307],[-60,-1422],[2764,1356],[-452,1373]],[[159718,836983],[-4078,-138],[-2403,3863],[2182,-4682],[2444,-3138],[-1641,2784],[123,902],[1602,-244],[1771,653]],[[294473,836638],[-896,1402],[-1408,66],[1583,-1987],[721,519]],[[287370,837457],[1688,160],[-1186,2129],[-502,-2289]],[[226959,845332],[-1977,-3354],[-895,360],[-623,-1640],[1940,770],[1679,1606],[709,1631],[-833,627]],[[217431,847801],[-739,1057],[-2498,-3777],[890,-3675],[-1994,-3118],[2283,1800],[1750,3923],[-717,578],[1529,2656],[-504,556]],[[224031,819892],[-1513,331],[-906,2234],[-20,2267],[993,1014],[-1359,95],[100,-3610],[-1444,-985],[2419,-1946],[1424,-267],[306,867]],[[288610,823079],[-1655,-222],[1641,2468],[-731,426],[-1104,-1648],[-2553,-1367],[1384,-1103],[3449,1143],[-435,-1247],[3825,2261],[-3126,-11],[-695,-700]],[[238232,823064],[293,934],[-2229,-387],[1936,-547]],[[307358,825126],[-1409,1693],[-785,-1418],[2194,-275]],[[249008,823252],[2079,32],[-1092,680],[-987,-712]],[[324512,825405],[-1589,-197],[-1719,3075],[-270,-2147],[-2602,-1459],[-1492,1564],[-1366,3216],[-533,-1128],[360,-2072],[598,1214],[2607,-3549],[-532,-2308],[1414,-406],[-675,2221],[1942,1945],[2048,-1578],[1809,1609]],[[239103,829583],[-1539,-201],[-796,-2306],[3100,2177],[-765,330]],[[210011,831510],[-625,870],[-1711,-523],[0,-1552],[2336,1205]],[[296716,831144],[381,-900],[2277,1985],[-2658,-1085]],[[200587,831811],[341,3160],[-1051,-1398],[710,-1762]],[[198441,839505],[801,-1889],[-1089,-3686],[1506,2928],[134,1892],[-1352,755]],[[72301,858817],[-990,131],[-4216,-1503],[284,-1203],[3138,692],[1784,1883]],[[189398,851653],[-1446,1288],[-156,-1182],[1679,-1461],[-77,1355]],[[214341,850290],[-320,1200],[-1828,-734],[-127,-2899],[1236,-540],[-55,1497],[1094,1476]],[[196575,855010],[3878,528],[1660,785],[-6410,1314],[-4764,-5137],[519,-1174],[1843,746],[1460,2225],[1814,713]],[[45922,862634],[-1068,118],[227,-1580],[841,1462]],[[237151,884822],[-4381,983],[1163,-1590],[3218,607]],[[200152,884438],[1232,-1804],[1100,618],[-602,1404],[-1730,-218]],[[197160,876811],[-4173,820],[-2413,-1321],[-2524,-3480],[-3534,-686],[-2077,2354],[-1088,-236],[-1597,1370],[-2081,700],[2129,-1535],[-52,-1359],[1818,-2182],[-2561,-761],[-1090,-3303],[-2260,868],[-1431,-1376],[5015,-1535],[4558,940],[432,1804],[1965,529],[2115,1386],[1725,2991],[2287,2467],[2858,615],[-4271,-195],[1696,1256],[3781,-660],[773,529]],[[227864,875283],[1455,-753],[-604,2622],[-2211,-210],[1360,-1659]],[[174114,879194],[-2171,2142],[-1464,-1382],[2469,-1326],[1166,566]],[[223316,877713],[1289,-177],[-572,1443],[-717,-1266]],[[216848,879267],[-278,-1756],[2068,-1532],[1129,1719],[-638,1275],[1133,2222],[-3414,-1928]],[[75167,922797],[-1865,997],[-1794,-452],[1671,-1071],[1988,526]],[[235418,889826],[-1894,-1],[1059,-1426],[835,1427]],[[170371,889803],[926,-1297],[190,2480],[-1558,109],[442,-1292]],[[183061,892047],[4868,-1480],[-3543,1706],[-1325,-226]],[[172660,898241],[-5222,-665],[-2009,496],[1003,1534],[2743,1510],[-2168,714],[-5842,-2330],[-8562,-2301],[683,-1816],[882,1255],[3065,-105],[1057,709],[4826,-967],[-4470,-3583],[97,-1239],[-1648,-959],[3904,-918],[1368,2385],[2457,1431],[313,-1871],[-2558,-2428],[670,-314],[4694,3443],[-1244,1057],[1024,1162],[4406,-528],[-460,1256],[1442,2062],[-451,1010]],[[205669,860170],[-968,611],[-187,-1600],[1155,989]],[[215141,862093],[1461,-2328],[380,1831],[-1041,2430],[-800,-1933]],[[180383,862136],[-2062,-446],[1417,-860],[645,1306]],[[211567,863464],[469,1666],[-2297,-644],[1828,-1022]],[[246263,806680],[1135,-86],[1416,976],[-493,565],[-2058,-1455]],[[227891,803055],[-791,2144],[-1435,1635],[524,1471],[-1651,3160],[-1053,-372],[1063,-1009],[428,-2393],[1539,-5501],[1376,865]],[[297500,807728],[-464,1449],[-2292,-2425],[-666,-3424],[3422,4400]],[[310402,809224],[-777,2114],[-1676,-587],[-28,-2263],[1127,-2089],[1084,1348],[270,1477]],[[223127,810497],[-459,3731],[448,3890],[-2325,1609],[-1629,-536],[773,-1941],[439,1346],[1102,-643],[766,-1808],[-463,-2670],[306,-2135],[1042,-843]],[[232297,805260],[230,3092],[-1539,3645],[-982,4778],[-2150,7742],[-137,2549],[-772,-2633],[1109,-1345],[-2860,764],[-863,-3896],[1196,-2873],[1726,-2635],[1012,-2317],[909,1444],[685,-1585],[-8,-2498],[522,1313],[897,-656],[-665,-1799],[140,-4678],[985,40],[565,1548]],[[549943,856209],[1470,468],[-621,-1769],[-4158,-3105],[-647,-4580],[-77,-4409],[-1475,-5009],[-3563,-524],[-1386,-1786],[-115,-2583],[-3578,87],[257,1673],[-1309,3550],[1046,1924],[-1283,1709],[-1907,4808],[-1288,4490],[-210,3570],[535,-247]],[[531634,854476],[-1539,872],[-736,2390],[-1083,-3423],[-1559,-375],[-4034,-4745],[-1945,-736],[-2530,608],[-2356,2371],[-263,2898],[1352,-846],[-409,2542],[1256,1551],[-2893,-2338],[-641,356],[481,2466],[2520,1121],[-1338,185],[2188,3226],[-1013,-363],[-1830,-3085],[-1066,-935],[369,5343],[-653,2776],[2710,469],[2933,-155],[-1065,684],[-3701,-584],[-893,1928],[933,308],[-1141,1334],[598,2662],[3360,2673],[3717,-152],[985,1100],[-3515,-417],[635,1522],[3033,790],[1331,1316],[-504,1315],[3607,530],[784,-1359],[2170,391],[-97,970],[1793,1065],[-178,1447],[-1033,-1651],[-2808,-1472],[-859,1617],[2641,3694],[2662,1931],[-96,1372],[1862,1204],[315,2306],[2350,3925],[257,2429],[2493,2835],[3683,755],[-2654,55],[1340,1886],[1399,-731],[-449,1862],[-1451,-535],[742,1664],[2665,1616],[702,-2026],[1087,4379],[2052,1029],[5018,5619],[6210,1572],[-214,1305],[3691,837],[1593,-2260],[337,1506],[2891,2692],[957,1816],[2788,-920],[-1374,-1782],[-638,-2626],[4064,4762],[217,-2979],[2696,2473],[115,1562],[2209,-687],[-629,-4073],[1232,2796],[1369,599],[5116,-3473],[-2828,-1055],[-3180,289],[2278,-998],[96,-1165],[3428,20]],[[585749,918146],[1619,-556],[1477,1564],[2659,-1195],[-2125,-461],[559,-1156],[3629,-1000],[6038,-702],[5202,-2960],[1943,-1993],[7045,-3805],[1090,-2984],[-472,-2272],[-1854,-2249],[-3423,-1864],[-2477,-401],[-8011,1964],[-1914,1276],[-4651,1378],[-610,1440],[-2875,442],[3672,-3732],[398,-1198],[2089,-617],[1871,-2137],[-1055,-2777],[1103,-2428],[183,-2524],[2160,-1076],[1994,-2224],[2992,-1123],[1747,1259],[-326,1744],[-2139,523],[-1820,2600],[985,1926],[1792,-380],[1337,-1361],[4857,-1786],[1908,1194],[-1796,3384],[51,1470],[2431,2165],[2178,948],[2041,2348],[2841,-617],[2420,-2411],[1067,3929],[-547,2535],[-1415,917],[1231,4391],[-157,1964],[-2125,1667],[4650,-181],[2261,-582],[2218,-3738],[-3227,-540],[-1637,-2410],[1730,-980],[1177,-1969],[1407,-313],[3232,1041],[608,3603],[2786,872],[5447,3665],[4181,1530],[4050,2297],[358,-3321],[-1862,-994],[4447,-389],[1546,2168],[1738,480],[3009,-562],[2906,1989],[2456,689],[870,-1586],[-754,-1742],[2218,-132],[-4,1685],[1648,134],[1234,1527],[-2119,3579],[2348,1544],[6515,-1044],[7565,-3785]],[[683568,913720],[921,-525]],[[684489,913195],[1628,-440],[3802,-3313],[2137,3770],[-1660,97],[-1492,3039],[-3338,1062],[1335,6394],[-1284,348],[264,2874],[3755,2373],[2138,5847],[1684,1349],[5153,96],[3644,-1317],[-521,-3626],[-1979,-3149],[1859,-2350],[330,-4111],[-641,-1080],[161,-7077],[791,-1571],[2044,-1426],[-1135,-2330],[36,-1874],[-4447,-6544],[-1700,-1258],[-2951,1762],[-2399,-339],[503,-1242],[3181,-1401],[4382,-565],[1390,1860],[3818,2575],[785,2481],[1930,2087],[-1050,3876],[523,1958],[5221,1346],[2165,-3014],[-178,-4094],[1390,-1121],[3465,-1],[-3706,964],[213,2598],[917,408],[-956,3814],[-4583,1967],[-3295,-856],[-2326,143],[-1159,3510],[2176,5162],[-3492,5133],[1626,2370],[3668,1777],[-571,3952],[1619,-92],[1033,-2964],[-1372,-2860],[235,-2794],[2162,-730],[4109,-301],[2772,-1030],[-1042,1614],[-2032,268],[-3247,1682],[-777,1866],[2331,726],[1887,-1131],[1894,653],[-2114,1421],[2809,1202],[2609,-86],[3724,-1726],[2079,-2032],[4097,15],[-190,-1948],[-1652,-948],[-179,-4244],[1696,2437],[448,-2219],[-968,-2149],[2928,1948],[-1624,3301],[1167,2908],[-3855,3810],[-3768,1485],[-882,3542],[205,2858],[8226,581],[8462,1349],[1218,-415],[-3340,-1963],[3592,723],[248,1562],[-2852,3226],[2908,-352],[-3997,1668],[2391,220],[2833,2650],[6982,2734],[9347,1558],[-1606,1308],[4455,456],[4166,-414],[1172,-1130],[6013,2082],[3232,-632],[-2833,2042],[5662,264],[405,2757],[5948,3767],[2455,616],[5222,-1431],[-1417,-1488],[-3284,-805],[7609,-399],[1356,-639],[-2197,-2093],[2737,-375],[1121,1235],[8576,27],[5992,-2794],[1261,-4744],[-2226,-2581],[-8568,-4106],[-4558,-3720],[-2580,-434],[-3005,-1853],[-1335,-2793],[2138,1794],[3535,200],[5847,1772],[2813,1531],[-3098,-48],[1413,1747],[5238,-1828],[3524,-363],[-719,-1115],[6058,1441],[8645,-669],[-55,-2033],[3667,-1586],[9472,-142],[1282,1412],[-880,2012],[3009,1315],[6012,-2489],[1330,1261],[5159,-2117],[-805,-1749],[1810,-1125],[-2311,-1007],[2759,-1301],[-1324,-1398],[-3143,2100],[5440,-7788],[3876,-2235],[1124,940],[3033,6073],[2145,-2577],[3546,-617],[3283,1443],[5443,-2391],[762,2010],[3031,-719],[2152,277],[-955,3003],[1372,1252],[-2661,-274],[1180,1971],[4106,538],[-1031,1795],[3759,-1002],[4040,-134],[7603,-1516],[-5788,-1087],[6736,258],[-2457,-569],[264,-2725],[4048,3445],[6221,-970],[1054,-1902],[-2313,-280],[2813,-1688],[4226,-1327],[2574,-2681],[3570,269],[5836,1278],[5950,-333],[4697,-2309],[773,-2014],[-483,-3109],[2995,-1057],[347,-3011],[1472,-1143],[-79,2810],[2330,1597],[4955,416],[982,-653],[6586,-647],[2067,1424],[1449,-965],[425,-1812],[2799,-1137],[831,-1739],[2578,233],[1271,1302],[-1147,3188],[-1171,256],[905,2850],[5757,-825],[1994,-856],[7863,216],[2269,-1270],[5343,-1533],[3200,-2392]],[[999999,913406],[0,-23201]],[[999999,890205],[-1534,-1453],[-2578,-1298],[-2142,676],[-1582,1760],[-307,-1347],[-2798,1032],[1860,-1991],[1824,884],[126,-1953],[1446,-1316],[767,842],[1169,-2365],[396,-2517],[1497,-2075],[663,-2979],[-1249,-2174],[-3060,1343],[-2019,308],[-7159,-3859],[-3033,-1372],[-1366,-1833],[-765,369],[-1287,-2412],[-4957,-3714],[-715,-2781],[-1023,602],[-2100,3133],[-3025,-131],[-3260,-1581],[-1757,-2575],[-543,633],[600,2995],[-3521,-2288],[-182,-1409],[-1784,1169],[-1657,-100],[-1028,-1222],[-381,-3154],[-746,-1674],[-2397,-3393],[-504,-2194],[1408,-1841],[698,1066],[1377,-1536],[-1207,-1950],[65,-3236],[1259,-732],[221,-2698],[-801,-1113],[-1164,1112],[1139,1715],[-1527,-727],[-1121,-1834],[-988,-4334],[1045,-3589],[-1055,-1299],[-2647,50],[-1940,-2088],[-641,-2401],[504,-3875],[-1220,639],[-2714,-2157],[-404,-3368],[-1000,-2934],[-3766,-4979],[-567,2028],[-496,7096],[-739,2945],[-1330,11009],[-181,2867],[449,4287],[739,3691],[2072,2708],[690,1859],[-515,1670],[1830,304],[600,1306],[1512,33],[2295,2362],[2252,4166],[2798,2961],[2496,3111],[694,1588],[2693,2150],[2048,793],[-435,644],[1255,1886],[561,5617],[1087,1057],[2217,139],[-3169,1200],[-2567,-862],[-895,-4499],[-1713,-767],[-4518,-5384],[-1646,-680],[570,2293],[-1635,-408],[258,1985],[1206,2972],[-2125,-438],[-1321,1202],[-2796,-1000],[-1669,269],[-2193,-1887],[-139,-1232],[-2157,-2935],[-2451,-2372],[-1884,-3219],[-397,-1806],[2825,-997],[-19,-1008],[-1950,157],[-1242,-836],[-1806,825],[-1329,-1633],[-1338,517],[-2983,-896],[-572,1229],[3166,835],[-2152,1781],[-2274,191],[-2846,1268],[-2349,-1410],[325,-1479],[-1824,779],[-2064,-863],[-2715,1116],[-1682,-1532],[-1047,1274],[-6562,-256],[-3241,-2195],[-752,-1507],[-2972,-3159],[-661,-2361],[-4957,-5024],[-2696,-4895],[-4212,-4663],[-2536,-2423],[-14,-1255],[1651,-875],[2626,220],[-217,-4839],[1211,104],[-164,1818],[2052,-1077],[-1703,-2178],[1435,-111],[2308,1531],[352,2969],[1733,-752],[1077,498],[1777,-2752],[2931,-3724],[-614,-999],[-31,-3833],[875,-1126],[-328,-1526],[-1207,-1782],[-680,-2297],[-587,-4066],[114,-5627],[-963,-6354],[-2217,-3770],[-1031,-2986],[-1152,-1933],[-694,-3043],[-1809,-4295],[-2450,-3835],[-1837,-4040],[-743,-685],[-1087,-3191],[-979,-1832],[-3949,-4122],[-1526,-788],[-1253,1060],[-1126,44],[17,2549],[-1231,-1294],[-199,949],[-1768,-3728],[-1247,180],[-62,-2097]],[[863019,755336],[-639,-4],[-1947,-3493],[-132,-5065],[-3900,-4866],[-2046,-1504],[-482,-3402],[1088,-733],[1634,-2729]],[[856595,733540],[678,-2651],[1990,-5341],[385,-3155],[-196,-4087],[472,-9],[-428,-3275],[-569,-1872],[-1953,-479],[-186,-1366],[-1133,898],[-892,-399],[-229,-1566],[-855,-1345],[-216,1729],[-971,-1873],[-1017,-739],[-741,2127],[721,146],[-647,2703],[548,2920],[636,722],[-493,2354],[-145,3126],[-752,1049],[1001,881],[1057,-672],[-588,1703],[-312,3485]],[[851760,728554],[-733,572],[-704,-803],[-965,1436],[-1143,-1543],[-1026,1224],[718,743],[-1544,429],[1046,2533],[673,643],[-424,1222],[700,2469],[-134,1412],[-1627,1371],[-380,-847],[-768,2304]],[[845449,741719],[-712,-966],[-2983,-992],[-1936,-1821],[-1902,-2969],[-1351,-790],[-159,1121],[1593,1113],[385,1646],[-1508,-11],[671,2726],[788,626],[1316,3503],[-1155,1779],[-1901,351],[-1932,-3972],[-2466,-1945],[-1018,-2930],[-869,-1431],[-1706,-589],[-713,946],[-712,-547],[-630,-3017],[1269,-2617],[2569,-833],[415,-2027],[-379,-2189],[1381,-1223],[3612,4201],[2473,-2213],[1156,406],[1696,-747],[-1091,-3371],[-949,745],[-2619,-2142],[-858,-2545],[-1740,-1820],[-1737,-3316],[-634,-3276],[2779,-2505],[843,-4073],[1016,-3683],[-48,-2104],[1521,-1715],[7,-982],[1191,-1815],[95,-1163],[-2480,983],[-1260,1401],[-933,-828],[1475,104],[2626,-3934],[604,-2386],[-974,-450],[-1471,-1675],[-490,-1206],[-1032,196],[510,-1508],[1461,998],[1440,-1911],[1125,-644],[-1601,-2286],[1208,719],[-65,-2790],[-557,719],[-749,-741],[603,-715],[-527,-2187],[353,-1628],[-1399,-451],[-1420,-4205],[-132,-1555],[-725,-1311],[-534,-2520],[-568,-363],[-161,1398],[-518,-1334],[676,-1700],[-1162,-1656],[433,-303],[-73,-3765],[-1240,274],[-658,-2876],[-756,-553],[-213,-1512],[-1314,277],[-86,-2257],[-1190,-2425],[-448,22],[-2325,-2883],[-441,-2417],[-608,210],[-2093,-1555],[-840,583],[-950,-1188],[-562,821],[-271,-1341],[-800,71]],[[817405,638260],[69,-246]],[[817474,638014],[-64,-1208],[-701,1282]],[[816709,638088],[-1100,2071],[2,1576],[-803,-1277],[616,-1884],[65,-1758]],[[815489,636816],[-446,-704]],[[815043,636112],[-2304,-2379],[-1784,431],[-948,-1721],[-1628,-281],[-1249,-1763],[-434,735],[-640,-2841],[919,-2016],[-1078,-1508],[-920,2121],[-308,3020],[693,2068],[-1075,340],[-1284,-579],[-1671,2751],[126,-1382],[-1535,-968]],[[799923,632140],[-1563,-1322],[-682,-1991],[-1336,305],[194,-1571],[-654,-2643],[-1483,-2073],[-1006,-5763],[740,-2748],[1697,-3294],[-56,-1344],[1948,-4868],[1816,-3409],[543,51],[1791,-5021],[409,-626],[732,-3921],[607,-5093],[-88,-3419],[422,-1916],[59,-2111],[-628,274],[-56,-5457],[-589,-2301],[-461,-124],[-1526,-2258],[-2805,-3175],[-708,1553],[-291,-1645],[-1216,-501],[892,-1077],[-527,-1521],[-1275,2144],[1030,-2372],[-360,-1571],[-1519,2634],[782,-1938],[155,-1640],[-1854,-1799],[-1073,-2749],[-956,-187],[208,5975],[687,1747],[-180,986],[-1012,607],[-659,1430]],[[790072,566398],[-1359,1039],[-1124,107],[527,1691],[-527,1520],[-1054,-1380],[-77,3240],[-531,1458]],[[785927,574073],[-944,2940],[-150,-555],[-1405,2504],[-863,933],[-774,-418],[-1616,567],[276,4250],[-852,529],[-1773,-996],[201,-1822],[-350,-2106],[70,-3077],[-1005,-4194],[-390,-3396],[-895,-3376],[-11,-3470],[647,-3083],[917,596],[502,-1193],[156,-2617],[885,-2386],[484,-4894],[-822,1693],[738,-3201],[1651,-1937],[1334,26],[837,-2314],[837,-1377]],[[783612,541699],[665,-416],[540,-1834],[1244,-2000],[1204,-3997],[147,-2707],[-296,-3698],[254,-1472],[-39,-3481],[1035,-2089],[1129,-5081],[-117,-2121],[-1338,502],[-596,-711],[-342,1283],[-1750,1832],[-3976,6100],[12,2182],[-1624,4224],[-280,4064],[-728,5542],[-25,2349],[-623,2712]],[[778108,542882],[-1175,2576],[-276,2837],[-662,98],[-855,3055],[-1311,2704],[-438,-983],[-508,1450],[370,5139],[920,5332]],[[774173,565090],[-389,-921],[-272,3797],[586,1843],[183,3583],[-292,869],[167,2884],[-335,5549],[-918,3387],[-266,-509],[-137,3044],[-800,4132],[-283,6023],[-350,853],[137,2596],[-716,387],[-549,3193],[-578,1513],[-171,-1697],[-796,-2767],[-2387,-2339],[-1038,-2644],[-156,1839],[-1084,-1273],[-138,2159],[-643,-1649],[162,2929],[-1378,-2265],[1014,9200],[-439,3746],[-943,3836],[-129,2596],[-321,-2297],[-622,754],[-590,2030],[922,-776],[481,1199],[-1766,3658],[-1001,98],[180,1793],[-956,-486],[-1107,2940]],[[756455,627897],[-745,2269],[-133,3021],[-509,3223],[-957,3887],[-914,-1604],[-571,-101],[-974,3181],[-444,-2263],[429,-2924],[-997,-2539],[-443,340],[384,1596],[-710,-793],[-200,2161],[-194,-2394],[-1273,-1554],[-722,898],[-118,1306]],[[747364,635607],[1,-2601],[-852,-413],[-287,3185],[-158,-2739],[-1466,204],[387,2639],[-1439,-2880],[-1605,-905],[-669,-1564],[322,-3179],[-625,-2292],[-1308,-2333],[-1957,-1342],[-320,1202],[-825,-1629],[774,34],[-1863,-2969],[-1852,-4934],[-1250,-1320],[-1266,-2730],[-1681,-1985],[-865,-2002],[-64,-2229],[-1380,-1365],[-1322,45],[-854,-3428],[-923,809],[-980,-1091],[-667,-3773],[311,-2939],[-150,-2166],[642,-5041],[-315,-3976],[-1031,-4156],[-290,-2450],[264,-2242],[-29,-5179],[-1243,-99],[-1096,-3690],[-46,-2456],[-1550,-969],[-637,-1268],[-367,-3000],[-1507,-1814],[-1530,1949],[-1148,2935],[-637,3255],[-227,2814],[289,-30],[-1178,5107],[-552,3422],[-1464,4122],[-1184,6042],[-277,3497],[-801,4900],[-1203,3437],[-48,1909],[-1266,3894],[-385,2403],[-504,6884],[-793,6287],[375,2003],[-475,-270],[-98,3224],[-366,1844],[593,4338],[-187,3282],[-557,2042],[1136,1408],[-1331,-18],[436,1632],[-1093,1287],[-748,-2169],[602,-1730],[-663,-2224],[-2752,-2469],[-848,9],[-1645,2098],[-3107,6530],[-70,1117],[814,-592],[2502,1702],[731,2356],[-2155,-1252],[-1191,530],[-1653,2023],[-620,2260],[998,1663],[-1505,-1512],[-194,1543]],[[689347,646059],[-1380,-275],[-997,2156],[-383,3443],[-1301,622],[-13,2164],[-750,2068],[-2080,-1304],[-2509,-284],[-326,-730],[-1409,885],[-1653,117],[-182,-843],[-2552,260],[-715,-710],[-1588,19]],[[671509,653647],[-584,340]],[[670925,653987],[-336,-554],[-2079,1067],[-2911,718],[-1583,83],[-689,813],[-1344,156],[-2721,1248],[-640,3435],[-339,3164],[-470,1093],[-1269,654],[-1961,-1320],[-2095,-2493],[-697,-283],[-1105,1112],[-1504,172],[-697,1289],[-2120,2252],[-599,1737],[-1237,1231],[-1012,122],[-1076,1697],[-1120,5518],[-557,497],[-71,1620],[-1335,2969],[-271,1643],[-1435,-1005],[-1391,1647],[-1410,-2041]],[[634851,682228],[-1577,121]],[[633274,682349],[457,-2431],[-1161,-922],[906,-364],[1086,-4814]],[[634562,673818],[920,-3459],[65,-1391],[1223,-1372],[466,-1847],[1614,-2085],[552,-2512],[-426,-1742],[1462,-6068],[685,-1762]],[[641123,651580],[-116,3883],[668,3180],[720,1018],[780,-1486],[-161,-2238],[324,-2232],[-483,-2842],[-445,-362]],[[642410,650501],[117,-1580],[718,-322]],[[643245,648599],[938,-1782],[958,59],[1103,944],[3459,-460],[1399,1192],[972,3153],[976,1370],[1179,2705],[1163,1752],[386,1592]],[[655778,659124],[971,1567],[-367,-4008]],[[656382,656683],[251,-3978]],[[656633,652705],[701,-3015],[1609,-3244],[3773,-1654],[2659,-6310],[800,-412],[-65,-1712],[-1190,-4272],[-1321,-2287],[-1171,-4182],[-1032,968],[-669,-1932],[-408,-3776],[268,-3494],[-1764,-678],[-1448,-1868],[-290,-2497],[-779,-1274],[-2198,-637],[-622,-1527],[112,-1209],[-643,-2030],[-2766,-198],[-1273,-1454],[-1457,-661]],[[647459,603350],[-2105,-2103],[-427,-1994],[121,-1786],[-1705,-1888],[-2991,-1769],[-1000,-1109],[-2270,-1263],[-838,-1074],[-1055,-2407],[-1884,-13],[-1618,-2289],[-1719,-1162],[-3143,-751],[-1718,-3098],[-1169,8],[-721,-877],[-1190,-312],[-1263,1318],[-676,2536],[141,2209],[-538,2199],[-189,3222],[-844,6515],[340,2236],[-112,2013]],[[618886,601711],[-279,2163],[-876,2284],[-248,1852],[-1511,2670],[-1446,4696],[-315,2393],[-992,3988],[-1884,3025],[-1298,1491],[-1444,4696],[148,1236],[-442,2149],[300,3028],[-246,2235],[-1996,6760],[-1025,1625],[-1046,630],[-1006,3130],[-89,2791],[-1751,4821],[-747,2903],[-1857,4962],[-1113,3569],[-1567,673],[454,2126],[475,5014]],[[597085,678621],[63,1193]],[[597148,679814],[-192,-460]],[[596956,679354],[-467,-1225],[-935,-7432],[-499,-1492],[-1277,1679],[-1424,3081],[-477,2994],[-985,2658],[-432,2679],[-572,-2033],[570,-1448],[185,-2335],[740,-2530],[1803,-3952],[7,-1722],[954,-3306],[183,-2372],[1684,-5675],[1747,-7204],[1638,-3184],[-675,-101],[-50,-2833],[486,-2940],[1477,-1881],[1783,-3744]],[[602420,635036],[154,-2431],[791,-2373],[-51,-6311],[153,-3192],[619,-4513],[1252,-1565],[777,-1816],[1133,-1448]],[[607248,611387],[839,-3424],[642,-4135],[434,-4787],[1352,-4717],[217,2046],[945,-2703],[2701,-2333],[1339,-3775],[1630,-2343],[428,-2222],[1103,-2063],[890,-922]],[[619768,580009],[814,-3073],[-382,-1306],[-1314,-1363],[-721,-1393],[1033,487],[929,-514]],[[620127,572847],[1686,-4239],[1482,-2098],[1546,39],[2427,2365],[2079,-533],[2333,2536],[1706,-205],[1820,1086],[734,-381]],[[635940,571417],[3254,1605],[1895,2692],[1285,-906],[-474,-2933],[157,-4022],[677,-1601],[-1262,-302],[-292,-5376],[-1098,-3454],[-908,-3824],[-697,-1405],[-252,-1795],[-1146,-3964],[-832,-4840],[-1111,-4024],[-1153,-3209],[-719,-2700],[-3046,-7176],[-2299,-4802],[-3141,-3941],[-1632,-2482],[-2403,-4558],[-4133,-9448],[-1242,-4279]],[[615368,494673],[-405,-1018],[-1381,-926],[91,-1009],[-773,-2048],[-1172,-882],[-297,-3331],[-1735,-7274],[-747,-1268]],[[608949,476917],[-1118,-7022],[152,-2688],[1662,-3242],[205,-861],[-716,-2926],[424,-2925],[-381,-2561],[409,-2957],[528,-1478],[396,-4278],[1888,-3258]],[[612398,442721],[412,-1168],[-581,-3972],[358,-3985],[-123,-2888],[260,-850],[-99,-4901],[280,-6374],[478,422],[47,-1919],[-603,-1920],[-164,-2121],[-1250,-2997],[-734,-2703],[-1673,-2115],[-439,-1068],[-2609,-1600],[-2502,-2944],[-2548,-6240],[-1877,-2196],[-1954,-3844],[-534,-55],[-141,-3859],[771,-1973],[792,-5004],[321,-4761],[307,1954],[227,-4967],[-569,-4947],[476,-156],[-288,-2054],[-784,-2193],[-1524,-1658],[-3500,-2605],[-1542,-2272],[-561,-2131],[718,-1564],[295,1093],[-191,-4536]],[[591350,345650],[-976,-8001],[-692,-2499],[-1410,-1869],[-1230,-2613],[-2907,-9432],[-3980,-7845],[-2765,-4500],[-2175,-2769],[-1800,-1412],[-1222,286],[-976,-1776],[-1765,222],[-488,-1157],[-3449,1089],[-882,-569],[-1984,421],[-856,-350],[-1269,-1798],[-2024,47],[-1473,-583],[-1415,-1911],[-1071,192],[-1491,2389],[-741,-83],[-63,1516],[-1269,-476],[314,1781],[-566,2762],[-1139,3520],[1110,1039],[167,3138],[-278,2251],[-1482,4286],[-1356,5446],[-664,4126],[-1396,4656]],[[545687,335174],[-2024,3861],[-1048,3432],[-1038,6330],[-341,3509],[-22,4103],[-932,4925],[-77,5455],[-196,1855],[340,1573],[-567,3037],[-968,2502],[-1452,5041],[-784,4337],[-1972,7452],[-1007,2286],[-889,3195],[-91,4457]],[[532619,402524],[211,3230],[-189,5168],[603,1172],[868,5904],[750,7107],[964,2430],[238,1493],[1205,1513],[1023,4192],[173,4493],[-351,2493],[-505,1261],[-917,4251],[-585,3881],[1001,2138],[54,1881],[-1434,6741],[-108,1642],[-839,2159],[-608,2949],[2127,1349]],[[536300,469971],[-1824,-720],[-550,1349]],[[533926,470600],[-101,2571],[-441,1898]],[[533384,475069],[-669,2598],[-1798,3848]],[[530917,481515],[-2174,5350],[-1634,2931],[-1279,3647],[-730,3520],[785,-1915],[568,200],[-1274,1777],[-676,3495],[876,800],[445,1316],[14,3790],[1375,-1447],[568,893],[-1264,598],[-615,1518],[814,145],[-75,2698]],[[526641,510831],[-569,636],[1169,4669],[-17,2234]],[[527224,518370],[410,4588],[-1090,4260],[510,326],[-711,1263],[-162,-852],[-1181,1003],[-228,2738],[-955,-164],[-51,1357]],[[523766,532889],[-895,902],[165,-2073],[-2802,-59],[-753,-890],[-2602,-632],[-1358,2112],[-568,2855],[7,1616],[-1458,3700],[-1193,1909],[-849,372],[-3944,-250]],[[507516,542451],[-3010,-903]],[[504506,541548],[-1210,-755]],[[503296,540793],[-658,-1653],[-1917,-314],[-1691,-1520],[-1246,-1624],[-2336,-1456],[-1009,-1294],[-1103,989],[-1987,944]],[[491349,534865],[76,235]],[[491425,535100],[187,14]],[[491612,535114],[-606,1212],[-305,-1213],[-2146,1061],[230,-471],[-2396,-544],[-344,387],[-2473,-1142],[-2803,-2207],[-1728,-1701]],[[479041,530496],[-1983,1414],[-2426,2753],[-3179,6061],[-1413,1377],[-177,918],[-1229,1322],[-600,1294]],[[468034,545635],[-627,1078],[-2091,1764],[-68,1655],[-1030,1131],[-210,1711],[-533,410],[-400,4945]],[[463075,558329],[65,719],[-1077,2776],[-91,1710],[-2047,1899],[-970,4048],[-743,50]],[[458212,569531],[-31,1196],[-940,446],[-116,4303],[-1068,-1067],[-387,1162],[-877,110],[-124,981],[-1091,1251]],[[453578,577913],[-143,4202]],[[453435,582115],[-172,1641],[746,-225],[3107,1067],[-1937,-207],[-848,-564],[-338,1387]],[[453993,585214],[-1143,4834],[-540,1407],[-1021,678],[1080,989],[843,2204],[856,3225]],[[454068,598551],[-2,2657],[526,3789],[744,3670],[134,2026],[-151,3752],[-357,2856],[-836,2125],[642,2519],[202,2611],[-609,2515],[-535,-108],[-705,2678],[-478,-1659]],[[452643,627982],[108,3383]],[[452751,631365],[217,3098]],[[452968,634463],[1591,4114],[411,2982],[1124,3861],[-259,562],[2390,4173],[508,1913],[170,3155],[1057,5033],[2329,2852],[888,4144]],[[463177,667252],[223,1310]],[[463400,668562],[630,1531],[2675,1275],[1544,1497],[970,1965],[1651,2081],[1760,4409],[516,1778],[40,2004],[-618,1602],[185,4187],[1281,3920],[283,2880],[1804,3642],[819,1109],[2053,1575],[1837,1948],[1522,4781],[1190,5982],[1797,693],[-167,-933],[1390,-2749],[1410,-709],[1768,702],[1353,-242],[650,996],[368,-1656],[1723,-140]],[[493834,712690],[851,-59],[1603,1600],[1163,1802],[1365,1144],[1317,231],[1297,2140],[2062,1528],[3711,480],[1053,1089],[2241,662],[2719,1],[1852,-1309],[2290,1557],[660,874],[1225,-985],[768,1024],[1961,-1398],[1851,479]],[[523823,723550],[3088,2388],[1412,-797],[600,-2808],[1782,2018],[202,-1175],[-1670,-3263],[181,-2584],[1149,-1501],[322,-2332],[-1626,-4120],[-1306,-1974],[262,-2142],[1566,-1988],[1005,287],[328,-1859],[839,-398]],[[531957,701302],[2153,-1916],[1316,-341],[1472,673],[2649,-1382],[767,-1009],[1843,-710],[507,-1371],[381,-2980],[582,-1365],[1159,-959],[1829,-295],[1577,-789],[2336,-1802],[2073,-2885],[986,-14],[1172,1187],[1215,3497],[-624,4378],[542,2377],[1388,2141],[2819,2116],[1532,-113],[2509,-1775],[544,-2399],[1420,-326],[922,-886],[1540,40],[947,-786],[349,-1352]],[[569862,692256],[644,-843],[1419,641],[3763,-1440],[1999,-1662],[1520,-278],[1548,-1304],[3675,3716],[1685,31],[140,763],[1312,-790],[1013,493],[-328,-1475],[919,-1183],[616,967],[777,-1110],[3609,665],[821,839]],[[594994,690286],[776,1554]],[[595770,691840],[558,1842],[1195,7038]],[[597523,700720],[1398,5619],[100,1280],[912,2257]],[[599933,709876],[-92,3523],[-496,2060],[356,2044]],[[599701,717503],[-227,2330],[1049,2068],[-388,1491],[-1421,-1858],[-2600,1111],[-2518,-3569],[-2500,-866],[-1158,875],[-989,2084],[-1859,1574],[-1968,383],[-446,-3290],[-2207,-910],[-1516,1425],[-292,1755],[-2040,702],[-1800,-814],[1629,2100],[-2481,-56],[517,855],[-878,1334],[-392,1769],[429,1724],[-2616,1769],[619,2087],[226,-1250],[1399,-17],[-930,1741],[694,1050],[-921,2402],[403,1604],[-1983,-566],[189,3096],[1547,2430],[1518,328],[530,-803],[4388,617],[-270,1223],[2464,637],[-1334,422],[-887,1174],[285,1265],[2143,-416],[1182,273],[1292,-664],[1236,135],[564,1259],[2357,2426],[2985,1706],[3804,-360],[711,631],[809,-1983],[2094,-273],[354,-1516],[918,-972],[745,598],[801,-1061],[3652,-1540],[2904,1078],[2330,-859],[1929,1482],[1529,1812]],[[615305,750685],[703,2681],[-762,4084],[-1812,2395],[-2384,2111]],[[611050,761956],[-3503,5143],[-1489,780],[-916,1654],[-1222,217],[-574,1401],[-1539,916],[807,967],[1961,517],[61,1641],[959,2333],[1327,253],[-2016,3233],[2041,163],[-174,885],[2375,1733],[-272,967],[-2726,-1051]],[[606150,783708],[-1863,-100],[-566,-935],[-2945,-1529],[-1257,-203],[-1519,-2044],[-138,955],[-1058,-1485],[481,-2897],[1487,-2311],[1701,843],[1124,-353],[-505,-1944],[-1453,-356],[-1105,552],[-1069,-1754],[-1030,27],[-2241,-2485],[-1276,983],[290,3224],[-1768,1484],[-1141,330],[3214,3218],[-1285,1355],[-2015,-546],[-1936,1428],[2217,1723],[-2905,291],[-2044,-667],[-1604,-4060],[-1715,-1092],[290,-2503]],[[582516,772857],[-412,-2468],[-1415,-508],[-1,996],[-1118,-3732],[-167,-3279]],[[579403,763866],[-333,-2091],[-921,37],[-569,-1241],[-112,-2585],[-1122,-1669],[1471,-2956]],[[577817,753361],[922,-2978],[1975,-1402],[-769,-1514],[-1690,631],[-1868,-637],[-671,-1694],[-1350,-1121],[-1581,-2504],[142,1418],[1495,1848],[-1907,-91],[-185,684]],[[572330,746001],[-2596,1587],[-865,-812],[-878,534],[-1096,-1325],[-888,141],[289,-1951],[-562,-1154],[-979,-43],[-1896,1653],[-103,-2717],[1934,-4432],[-1249,-179],[352,-1349],[-1025,-832],[1823,-1358],[1983,-2288],[246,-3350],[-1319,1783],[-1512,-783],[1258,-2596],[-910,-630],[-1211,1234],[748,-3118],[-33,-2888],[-564,1527],[-1122,-499],[-821,1937],[-522,-1728],[-860,2036],[-32,2726],[-1204,1855],[738,2029],[1170,779],[3043,-2191],[635,1290],[-2020,1555],[-2637,-694],[-998,375],[-926,3696],[-1330,1888],[-832,2265]],[[555559,739974],[-416,1979],[-1020,986],[-388,2442],[323,1843],[-57,2912],[380,2149],[-653,484]],[[553728,752769],[-2291,3340]],[[551437,756109],[-2361,2750]],[[549076,758859],[-229,244]],[[548847,759103],[-1894,2690],[-1415,895],[-1134,-140],[-2396,4366],[966,90],[-1599,2575],[-113,2217],[-1505,1523],[-964,-2975],[-934,1614],[-143,2422]],[[537716,774380],[394,419]],[[538110,774799],[-254,1086],[-3761,-1925],[-135,-1212],[827,-1620],[-764,-1455],[411,-2954],[819,-1357],[2425,-2509],[1239,-5224],[2377,-3774],[841,-702],[2209,32],[520,-1072],[-385,-1914],[3030,-2211],[2365,-2411],[1475,-3261],[-395,-1679],[-738,685],[-591,2033],[-2603,1054],[-1106,-3545],[188,-1308],[1436,-1530],[167,-2267],[-1710,-1678],[-38,-1811],[-1357,-2768],[-923,-16],[688,4582],[624,276],[-481,3522],[-920,3771],[-2060,1474],[-515,2544],[-1842,941],[-1025,2420],[-1791,48],[-1272,1338],[-2760,4846],[-947,804],[-1633,3039],[-1835,6419],[-2107,1774],[-1454,611],[-1901,-2982],[-1634,-900]],[[520814,764013],[-644,-421]],[[520170,763592],[-2132,-3121],[-1050,-574],[-1970,925],[-964,1280],[-1197,-341],[-1600,1220],[-2205,-2369],[-575,-1646],[443,-2868]],[[508920,756098],[102,-2884],[-3237,-3892],[-2916,-1335],[-3078,-7027],[-701,-2109],[340,-2709],[1129,-1798],[-1619,-1917],[-737,-1681],[-487,-3383],[-1404,-117],[-1306,-1945],[-1083,-2887],[-6054,-162],[-853,-1254],[-1382,-490],[-1261,-2357],[-1154,963],[-1254,4539],[-1089,1420],[-1449,-88]],[[479427,724985],[-1189,-1029],[-2121,685],[-1111,-528],[510,2361],[-186,6019],[-923,8],[215,1746],[-939,-71],[276,3599],[629,1210],[726,3773],[642,5036],[-618,4755],[280,646]],[[475618,753195],[240,1973],[-656,3107],[-856,1057],[1004,2118],[1736,621],[-23,833],[1553,1093],[1211,-1006],[4434,-72],[3174,-988],[2552,615],[1552,-876],[474,491],[1495,-749],[1507,470]],[[495015,761882],[860,927],[664,5901],[458,5762],[873,-1292],[-736,2528],[-319,3379],[-1778,1205],[-1140,3840],[615,875],[-1466,8],[210,941],[-4092,2172],[-1143,-86],[-1018,1283],[970,772],[-1087,2192],[4136,1783],[1499,-1801],[684,661],[2971,25],[-525,906],[-49,2351],[-759,2852],[1660,-21],[334,-1732],[2708,-540],[1613,898],[-640,1509],[2941,1748],[965,1505],[-38,2885],[926,1491],[1701,631]],[[507013,807440],[2292,1662]],[[509305,809102],[2434,52]],[[511739,809154],[-2159,914],[2038,411],[-655,1187],[1488,2953],[544,2967],[3844,3539],[2094,202],[1059,-942]],[[519992,820385],[243,2365],[2012,54],[1342,-1044],[352,2136],[998,304],[-73,3208],[-750,1921]],[[524116,829329],[-57,1149]],[[524059,830478],[-126,2562],[-1344,1075],[88,5966],[1409,-658],[280,1359],[-1356,754],[930,1534],[2599,718],[1134,2065],[1586,915],[-26,-2916],[-670,-3689],[1564,-587],[29,-1339],[-1493,-489],[-377,-2060],[-1645,-2204],[-381,-2688],[699,-660]],[[526959,830136],[112,-716]],[[527071,829420],[1105,-1889],[1633,-1020],[783,373],[-265,-2274],[1338,-301],[1977,1326],[1289,1771],[1259,-333],[1165,-1601],[767,73],[393,-1776],[1068,-720]],[[539583,823049],[660,-355]],[[540243,822694],[-380,1107]],[[539863,823801],[-496,92]],[[539367,823893],[109,450]],[[539476,824343],[5485,2015],[1038,1561],[1950,1041],[2949,643],[962,-2413],[851,-485],[1745,652]],[[554456,827357],[1028,2738],[1516,437],[1054,1728]],[[558054,832260],[-112,-611]],[[557942,831649],[-735,-1191],[1650,-280],[95,1022]],[[558952,831200],[37,968]],[[558989,832168],[-528,4734]],[[558461,836902],[70,4464],[1826,4428],[2294,908],[2035,-3759],[1789,-482],[1311,1874],[-224,3233]],[[567562,847568],[574,2866],[-2116,39],[-932,3317],[174,1629],[2461,1641],[2954,287],[182,699],[4070,-1117],[2883,200]],[[577812,857129],[4,1425],[2592,616],[556,1013],[2709,-748],[-1115,1906],[-1811,-23],[-1183,1090],[-362,1789],[-1987,-836]],[[577215,863361],[-1544,15],[-4404,-1218],[-3649,-1723],[-2442,-333],[-1657,1361],[-1123,-1106],[339,2081],[-3191,1279],[-210,2198],[682,3698],[-972,2359],[-423,3752],[1520,2466],[-294,978],[2152,629],[590,1999],[1990,1471],[2860,3668],[777,1693],[2028,351],[166,3667],[-3312,1931]],[[567098,894577],[-2925,-414],[-2045,636],[-312,-1452],[-1912,-1123],[42,-1465],[-1229,-2086],[1059,-2047],[-2102,-3527],[-3913,-2313],[-2077,-1772],[-398,-1673],[-1577,-387],[394,-1363],[-1296,-886],[-1222,-5185],[334,-5184],[1958,-658],[2309,-3023],[509,-1909],[-2795,-2357]],[[549900,856389],[-432,1148],[-1213,-340],[-2217,687],[-1193,-972],[2137,-11],[1086,-1029],[1875,337]],[[636756,779239],[-2981,-3555],[-1749,-1226],[-1238,-4225],[-913,-950],[1684,-3929],[410,-2866],[-127,-2813],[3082,-7052]],[[634924,752623],[1483,-3216],[333,-1632],[1526,-2620],[596,-43],[1043,-1761],[-1242,219],[-1021,-725],[-977,-6644],[-517,364],[-452,-1888],[50,-2251]],[[635746,732426],[589,-4549],[1081,-1013],[1835,-530],[1118,-2331],[1626,-1606],[1788,-759],[1189,43],[3289,1463],[1655,-299],[-155,3112]],[[649761,725957],[-252,3462],[172,5546],[-502,2047],[-1522,329],[219,2035],[1021,-365],[-322,2147],[-1636,19],[-457,2880],[583,3788],[560,-1262],[1306,-39],[412,-905],[1705,163],[924,1173],[-328,1791],[-1381,1931],[-690,3387],[-1895,16],[-540,-697],[-300,-4539],[-1022,3379]],[[645816,752243],[-89,1897],[374,3908],[-1766,535],[-958,1824],[-890,93],[17,1826],[-1308,4208],[-1388,787],[-93,1517],[1563,280],[1898,-579],[-1349,1662],[-49,1000],[1043,2237],[3098,241],[1859,-395],[-1185,1427],[1004,3666],[97,2829],[-706,1690],[-1202,215],[-1105,-895],[-2520,1603],[-2108,-1367],[-1165,-1452],[-1812,-682],[-320,-1079]],[[579890,406773],[-220,141]],[[579670,406914],[-1105,-546],[-671,-1286],[-690,-10],[-2147,-6749]],[[575057,398323],[576,1214]],[[575633,399537],[498,1027]],[[576131,400564],[826,2210],[951,844],[287,1338],[1698,181],[628,1169],[-631,467]],[[596829,424051],[8,920]],[[596837,424971],[-448,7467],[713,2757]],[[597102,435195],[-55,1606]],[[597047,436801],[-915,2270],[165,1707],[-397,4516],[-565,1769],[-903,1399],[-117,-964]],[[594315,447498],[86,-2168],[765,-2509],[-181,-705],[342,-6592],[-719,-2188],[41,-1803],[711,-3505],[-25,-2578],[835,-2199],[-59,-2417],[712,855],[1187,-2077],[-633,3686],[-548,753]],[[583319,439012],[-188,1258],[-1007,-1653],[65,-1400],[914,550],[216,1245]],[[590316,457091],[-1354,2401],[-631,-116],[447,-1971],[1538,-314]],[[581104,494978],[-351,228],[-450,-2385],[-164,-2606]],[[580139,490215],[167,89]],[[580306,490304],[1242,2470],[-300,1659]],[[581248,494433],[-144,545]],[[594373,505981],[-82,25]],[[594291,506006],[-1283,3],[-822,1245],[-139,-1043],[-2578,-1442],[-719,-834],[122,-1120],[-754,-2779],[125,-863]],[[588243,499173],[42,-439]],[[588285,498734],[229,-485],[-558,-4978],[429,-5130],[1171,3050],[1584,-1374],[713,620],[968,-717],[1092,2009],[-1480,404],[689,756],[-629,442],[819,952],[209,1512],[615,-40],[118,1925]],[[594254,497680],[365,913]],[[594619,498593],[45,2690],[383,818],[1656,983],[-266,1090],[-1202,-1532],[-841,2220],[-21,1119]],[[550098,499048],[149,1643],[-772,44],[623,-1687]],[[582340,501712],[626,1212]],[[582966,502924],[-236,1073]],[[582730,503997],[-387,11]],[[582343,504008],[-409,-359],[-361,-2723],[767,786]],[[580912,453401],[-726,882],[-1388,-3483],[-94,-2068],[1006,317],[1202,4352]],[[586662,453459],[-41,293]],[[586621,453752],[-1610,6282],[-212,3670],[-1086,2711],[-477,-209],[-675,1517],[627,2047],[-544,2923],[168,1754],[-569,1196],[94,2477]],[[582337,478120],[31,410]],[[582368,478530],[-801,3341],[-183,2968]],[[581384,484839],[-285,99]],[[581099,484938],[-369,-5798],[532,1280],[-362,-2874],[-21,-3186],[709,-2918],[-506,-1768],[893,-4769],[1766,-3432],[410,-3397],[794,-1827]],[[584945,456249],[-19,-482]],[[584926,455767],[-303,-1397],[928,-402],[684,-1335],[427,826]],[[593018,514450],[-202,523],[-1153,-1089],[-1262,-151],[1988,-1771],[-373,1661],[1002,827]],[[586953,517904],[-123,-430]],[[586830,517474],[-2165,-4070],[-5,-1348]],[[584660,512056],[322,-1170],[1043,2878],[1196,2183],[-268,1957]],[[605708,543686],[-517,-515],[-73,-2474],[733,2227],[-143,762]],[[500755,544260],[92,2515],[-132,4775],[-564,-668],[-1498,2472],[-1767,4265],[904,-3637],[1675,-2135],[-283,-1706],[850,-738],[478,-1638],[-200,-2189],[-850,-921],[-1222,72],[2049,-2460],[468,1993]],[[601901,519752],[-90,1900],[-625,906],[-572,2734],[-131,6867],[-621,-468],[-287,-4607],[330,-2413],[1272,-3754],[308,-1698],[416,533]],[[513014,566127],[-597,1548],[-7,-3130],[530,-1266],[74,2848]],[[790472,578337],[-362,1765],[-571,235],[-705,2460],[-782,165],[298,-1542],[1254,-2269],[868,-814]],[[540322,581615],[423,1277],[-906,-197],[-187,-1535],[670,455]],[[603570,574593],[283,-961],[568,2872],[-365,1103],[-1012,-223],[-261,-2127],[787,-664]],[[785773,614477],[-978,844],[245,-1141],[733,297]],[[589539,639047],[271,-551],[1877,3887],[-268,2056],[-1140,-4893],[-926,179],[-1993,-3425],[-939,-3194],[577,837],[905,2959],[1140,2091],[496,54]],[[752585,676072],[-1025,1705],[79,-2453],[946,748]],[[824182,677333],[-1536,690],[124,2955],[-646,-2607],[368,-1217],[1202,-1253],[488,1432]],[[835004,688893],[-392,-39],[-672,2672],[-642,-830],[-27,-1781],[1157,-714],[576,692]],[[750960,685556],[1857,1866],[-196,502],[-1819,-456],[158,-1912]],[[632245,686535],[-2077,1346],[-107,-1133],[2184,-213]],[[738577,687953],[-1352,630],[-67,-767],[1247,-724],[172,861]],[[748104,693080],[-970,734],[-1117,-439],[1587,-1436],[500,1141]],[[831685,699001],[-162,1684],[-748,-1323],[910,-361]],[[749194,703001],[333,-1261],[388,1327],[-721,-66]],[[621884,698374],[-912,2321],[-189,-2590],[862,-635],[239,904]],[[741047,689360],[-98,1041],[-960,-2790],[1058,1749]],[[731194,691121],[-361,1115],[-731,-517],[1092,-598]],[[827614,691459],[-1739,1048],[424,-1368],[1315,320]],[[770743,711929],[-1115,128],[456,-1016],[659,888]],[[771908,711114],[-346,1481],[-599,-1257],[945,-224]],[[779868,722801],[-1328,2011],[-885,400],[-850,-1816],[857,-1378],[2071,-529],[135,1312]],[[626850,728317],[-640,381],[-219,2714],[-698,0],[719,-6148],[475,-543],[895,1727],[-532,1869]],[[619412,731894],[916,848],[-73,1920],[-2128,-401],[-480,-1454],[1470,-136],[295,-777]],[[592937,734903],[-423,1618],[-409,-2100],[1042,-1478],[390,1123],[-600,837]],[[750778,745730],[-534,277],[670,-3123],[-136,2846]],[[624971,744746],[897,-2169],[941,273],[-774,1684],[-1064,212]],[[688863,749129],[401,-1223],[890,346],[-1291,877]],[[829457,701243],[710,1093],[-1035,1086],[-870,-1953],[24,-1928],[1171,1702]],[[620169,704535],[573,1320],[-1115,1822],[542,-3142]],[[868915,771622],[-4,576]],[[868911,772198],[-637,875],[-1411,-56]],[[866863,773017],[-184,-432]],[[866679,772585],[179,-2794],[823,-1261],[1234,3092]],[[728146,775844],[-613,3476],[-1063,599],[-286,-2349],[1962,-1726]],[[719418,781676],[-2373,-910],[-4556,241],[-1862,1081],[-1109,-477],[-2226,87],[-1475,-1921],[-313,-1431],[-1495,-3136],[1878,-3895],[-53,3183],[525,674],[-32,2094],[1371,615],[324,1592],[1433,1380],[576,-606],[1742,282],[2404,-597],[904,269],[1633,-929],[2424,160],[922,2078],[-642,166]],[[598330,772830],[-1552,3481],[-238,1599],[-2940,519],[1851,-1185],[1771,-2191],[248,-1615],[860,-608]],[[519093,779803],[-793,572],[-699,-995],[1492,423]],[[598060,786080],[-477,2063],[-370,-1496],[-2947,-274],[-691,-4032],[1396,3312],[1118,595],[1023,-674],[948,506]],[[527029,786288],[-240,234]],[[526789,786522],[-1282,617]],[[525507,787139],[1032,-944]],[[526539,786195],[490,93]],[[825635,792558],[1620,3199],[-490,1285],[-1798,-2969],[668,-1515]],[[757740,790146],[-2251,-998],[622,-1428],[1629,2426]],[[734765,788675],[-2596,2000],[-262,2671],[340,1138],[1998,1271],[-1664,2431],[446,1097],[-1459,-559],[2041,-2300],[-1745,-2249],[-101,-3485],[-1101,-120],[3342,-3218],[761,1323]],[[760553,795780],[-1500,966],[-1096,-395],[1480,-1392],[1116,821]],[[592130,795130],[-1395,3067],[-92,-920],[-2031,776],[2414,-2351],[1104,-572]],[[662809,774782],[-63,-283]],[[662746,774499],[-880,-3290],[-81,-2711],[618,-374],[836,1696],[-121,4023]],[[663118,773843],[104,375]],[[663222,774218],[640,1702],[1079,-1224],[186,-2401]],[[665127,772295],[-179,-732]],[[664948,771563],[-498,-1754],[1691,-3336],[620,1333],[58,2571]],[[666819,770377],[23,2136]],[[666842,772513],[-175,2227],[-643,9],[218,1988],[1582,-394],[2240,2499],[162,813],[-1538,1592],[-1140,68],[-663,-1693],[1804,3],[-214,-1962],[-2400,103],[-1211,-2325],[-46,1648],[-1706,-751],[-303,-1556]],[[742708,752661],[-166,982],[-1185,542],[-376,-1675],[1727,151]],[[703585,752117],[-1682,832],[-17,-1401],[1699,569]],[[660075,754430],[-215,659]],[[659860,755089],[-770,252],[-497,-1984]],[[658593,753357],[-112,-1155]],[[658481,752202],[812,-1186],[1311,750],[-529,2664]],[[553592,755111],[-214,-628]],[[553378,754483],[495,-417]],[[553873,754066],[-86,1194]],[[553787,755260],[-195,-149]],[[717506,757030],[-149,861],[-2533,-379],[-3085,-1239],[102,-788],[1829,-813],[1647,-51],[2189,2409]],[[516297,816124],[-1241,1845],[-1057,-3142],[1265,-546],[1033,1843]],[[703851,819125],[51,1726],[-1161,44],[1110,-1770]],[[634409,825632],[1183,-1358],[145,-1724],[943,-1659],[1879,699],[-2208,287],[365,2372],[1415,1722],[-1964,-1244],[-1308,1336],[874,1313],[401,3009],[1077,272],[1377,1644],[3786,1378],[-3237,-375],[-2032,-734],[-1385,-1939],[-318,-1994],[-914,-462],[-79,-2543]],[[554900,827208],[1780,1468],[-1158,167],[-1782,-2619],[1160,984]],[[787591,822480],[-1194,3030],[438,1154],[-296,2720],[582,2473],[-947,5896],[-1558,234],[-2102,-1663],[761,-2664],[18,2978],[1444,772],[1153,-552],[976,-4508],[-665,-3079],[514,-1751],[-596,-2483],[754,-2311],[277,-3377],[441,3131]],[[713185,829568],[-35,-687],[2794,-326],[857,1577],[-1329,746],[-2287,-1310]],[[607446,848975],[857,2297],[-657,304],[-3077,3613],[-399,-1849],[1374,100],[216,-1895],[-2564,1819],[3079,-4609],[1171,220]],[[587585,849699],[-646,1460],[-1172,-1555],[1818,95]],[[576897,854139],[-443,162]],[[576454,854301],[-1548,-1193],[1826,-4688]],[[576732,848420],[152,25]],[[576884,848445],[395,2763],[-382,2931]],[[541480,852979],[-1184,-1323],[-1071,-3768],[344,-681],[1911,5772]],[[545566,856089],[-2918,-151],[1284,-796],[1634,947]],[[538995,854750],[20,1368],[-2508,170],[280,-2440],[-1181,897],[-539,-4145],[1156,1315],[2108,700],[664,2135]],[[574388,886199],[1329,-1605],[1852,1547],[-3181,58]],[[808666,875704],[-89,-3371],[864,42],[70,3529],[-604,2084],[1769,920],[-988,-1704],[1908,-641],[736,1839],[-1092,1421],[-2668,-1308],[-1701,923],[-963,1693],[843,-3241],[1968,-830],[-53,-1356]],[[601163,868433],[-2027,3839],[141,2843],[-3354,2343],[246,-1205],[2030,-1123],[560,-1699],[-1394,-144],[-1142,1644],[-29,-4945],[2298,-1770],[1032,-2898],[1572,2012],[67,1103]],[[575103,870033],[-456,1418],[-1619,-283],[2075,-1135]],[[565308,868591],[687,2172],[1572,1448],[-398,753],[-1313,-1732],[-548,-2641]],[[574860,877079],[-457,-1440],[-2353,-868],[458,2157],[-1308,-1179],[806,-1433],[-2040,-5057],[865,-1794],[573,2204],[-180,2069],[654,1513],[3076,2356],[-94,1472]],[[581993,872761],[1135,1841],[-1847,695],[192,-2281],[-3509,461],[-859,2011],[1692,22],[-591,2050],[-2474,1261],[823,-1749],[-36,-2598],[978,-1509],[2521,-1952],[-1514,-494],[-577,-1386],[-660,1288],[-1517,-2391],[2228,-540],[-349,-980],[2557,1064],[-381,2511],[1023,-550],[1885,1843],[-720,1383]],[[597257,880953],[-1849,2047],[1683,-3970],[166,1923]],[[592892,880587],[-374,-949],[2109,-325],[-1735,1274]],[[583315,879692],[-2504,1663],[836,-2316],[1169,-221],[1433,-1873],[-934,2747]],[[590325,892960],[-1483,2117],[-1322,-558],[2805,-1559]],[[550002,895031],[-92,1328],[-1727,-154],[1819,-1174]],[[580190,897575],[-2219,-1640],[1544,169],[675,1471]],[[755379,910475],[-7289,-911],[1729,-560],[6655,1098],[-1095,373]],[[744889,916455],[-2083,2995],[881,-2910],[1202,-85]],[[780618,942300],[1406,549],[-1870,3023],[3704,-1323],[-412,1551],[8865,1674],[-2125,1828],[-648,-1651],[-2725,-820],[-4020,-206],[-4747,1302],[1105,-1681],[-2744,-1047],[3870,-856],[341,-2343]],[[579019,915436],[-3570,-2388],[840,-960],[2478,660],[252,2688]],[[748524,915745],[1750,953],[4056,-718],[-3761,1228],[-3777,-1505],[1732,42]],[[589764,898704],[-271,1975],[-1256,137],[1527,-2112]],[[584338,890689],[964,-760],[2410,692],[-3374,68]],[[603422,861159],[2045,-467],[-643,1444],[-1402,-977]],[[582879,867448],[1589,-1766],[1945,-4630],[961,-1418],[679,1946],[2225,-351],[1233,2762],[-1159,2814],[-2642,1894],[-2011,2000],[-2820,-3251]],[[779963,809216],[-1131,409],[-518,-5760],[697,440],[952,4911]],[[756434,802727],[791,-1756],[2112,1637],[-1302,2231],[-1826,-1453],[225,-659]],[[692465,805016],[-1413,-1035],[-246,-1466],[1570,-68],[-579,1015],[668,1554]],[[805177,833755],[-802,1378],[-1061,-1398],[-62,-2025],[-3079,-7966],[-1466,-2430],[-1286,-1172],[-1723,-3464],[-1088,-858],[-2485,-3589],[-3818,-1032],[1665,-1374],[1211,-59],[2699,1251],[1080,1513],[354,2173],[4538,2709],[1981,3088],[750,253],[-264,2743],[1133,362],[866,1690],[-183,1343],[1040,6864]],[[292109,640353],[-1063,1606],[183,1013],[880,-2619]],[[752159,635584],[-729,-143],[459,1554],[-494,3009],[472,-131],[544,-1945],[-252,-2344]],[[284284,648382],[233,-3023],[-549,79],[-635,2768],[951,176]],[[856273,662620],[-854,-1163],[-828,-2143],[211,2010],[1460,2654],[11,-1358]],[[285484,658185],[54,3465],[-910,2455],[1298,-2211],[-442,-3709]],[[281964,663072],[1525,90],[-2020,-1408],[495,1318]],[[656077,664210],[-649,-1303],[-1687,-315],[2408,2105],[-72,-487]],[[836135,638730],[-470,-4119],[-718,2555],[-710,1103],[-704,3598],[241,3313],[1308,4559],[1141,3284],[1536,1437],[932,-1787],[-805,-4998],[-450,-4183],[-505,-2710],[-796,-2052]],[[284045,651095],[-4,-1445],[-833,-1043],[-1084,2015],[766,2339],[512,370],[643,-2236]],[[450460,673524],[-463,1570],[712,168],[-249,-1738]],[[188325,676558],[-210,-1398],[-654,463],[122,1897],[742,-962]],[[185678,676836],[-1180,2138],[-19,947],[1070,-1606],[129,-1479]],[[838507,691292],[-1462,897],[355,668],[1107,-1565]],[[634098,680225],[-539,1034],[287,1066],[252,-2100]],[[457220,671475],[32,-1616],[-892,-536],[76,2191],[784,-39]],[[859589,671840],[-799,241],[1458,1587],[-659,-1828]],[[454626,672853],[-899,-2209],[-687,1970],[2184,1117],[-598,-878]],[[460564,671605],[674,3389],[352,-926],[-279,-1965],[-747,-498]],[[577340,717578],[-351,1436],[1432,1552],[-400,-2182],[-681,-806]],[[594456,712459],[-2952,-2899],[-1368,910],[-367,1326],[1099,1290]],[[590868,713086],[636,1300],[1436,-323],[2790,1526],[-1447,-1786],[173,-1344]],[[874812,707856],[-735,35],[935,1481],[-200,-1516]],[[566256,715245],[1279,-1017],[1135,362],[2095,-703],[748,-969],[1175,429],[212,-1009],[-4012,-653],[-253,916],[-2292,930],[-934,1004],[847,710]],[[543268,731152],[-1325,-4513],[541,-2638],[-505,-1929],[-1697,657],[-997,1807],[-659,-18],[-4083,4261],[830,2152],[467,-878],[715,921],[1449,-1123],[2290,265],[944,747],[2030,289]],[[884288,728792],[-272,1022],[715,1904],[198,-1484],[-641,-1442]],[[557256,732117],[414,-1853],[-980,1577],[566,276]],[[572483,731138],[-689,2109],[873,-197],[-184,-1912]],[[300269,747979],[-3587,-2408]],[[296682,745571],[-2327,-92]],[[294355,745479],[1273,1664],[4641,836]],[[526334,758316],[195,-4003],[-1012,-4413],[-1052,1205],[-672,4558],[409,1138],[2132,1515]],[[508736,740452],[844,-171],[-1045,-2725],[-1949,1847],[2188,2117],[-38,-1068]],[[555771,738334],[-1191,1739],[527,556],[664,-2295]],[[565042,735526],[1899,-1704],[491,-2672],[-1436,1073],[-1407,2370],[-1061,411],[1514,522]],[[573361,737722],[483,-1765],[-1176,-33],[-879,1032],[1572,766]],[[570660,741596],[-224,-1038],[-831,1135],[1055,-97]],[[511926,740758],[-1253,797],[1065,332],[188,-1129]],[[526755,746921],[481,-2266],[-523,-6785],[-363,-1273],[-1194,591],[-486,-1933],[-647,81],[-640,1653],[359,3754],[-211,2686],[-782,2127],[96,1550],[965,-374],[1824,2411],[1121,-2222]],[[864373,703794],[1134,295],[355,-889],[-481,-1352],[998,-116],[222,-2433],[-679,-1487],[-1097,-7039],[-1811,-2308],[289,1504],[-373,2662],[-184,-3198],[-1078,671],[337,1834],[-355,2899],[1238,3131],[-717,2804],[-708,73],[133,-1502],[-967,-746],[-685,3027],[222,762],[1957,1597],[331,1186],[1304,221],[615,-1596]],[[452247,699446],[1382,-656],[-1327,-214],[-55,870]],[[850907,701549],[29,1403],[1566,324],[-78,-1031],[-1517,-696]],[[873214,707667],[778,-176],[282,-2406],[-1005,-1257],[-543,-2139],[-1527,1562],[-961,-894],[-858,-3069],[-931,-471],[-408,913],[-229,3044],[-909,-535],[1549,2073],[392,1792],[971,-386],[1245,532],[344,1305],[1027,717],[783,-605]],[[892303,749827],[628,190],[20,-4701],[929,-1896],[502,-2646],[-214,-4344],[-674,-808],[-530,-3381],[-997,-393],[-501,-2300],[301,-2858],[-190,-2756],[-946,-2958],[-16,-2628],[701,-1980],[-1157,-1272],[-114,-1441],[-1378,-2177],[-261,2353],[722,1444],[-699,697],[-552,-3057],[-1074,805],[-454,-2600],[-690,-1303],[-97,2107],[-626,661],[-1078,-2904],[-1794,403],[-1338,-483],[592,1124],[-1084,198],[-91,1503],[-886,-2259],[891,-2099],[-1455,-872],[-1149,-3644],[-1287,-50],[-771,2042],[-209,2313],[791,1256],[-1791,1569],[-1477,-400],[-667,-1010],[-2294,-1332],[-2512,-447],[-255,-2300],[-1127,1264],[-2282,-452],[237,2469],[971,122],[1395,1856],[2964,4650],[1259,-311],[2327,477],[2667,1231],[424,-1313],[981,-132],[1153,1567],[-247,1321],[1922,4452],[404,3792],[1331,829],[-1107,-2074],[257,-1984],[905,-396],[477,1074],[2238,1581],[1570,3706],[1432,1769],[1766,7515],[51,2004],[-679,761],[567,2590],[-254,1680],[914,1246],[372,2494],[671,-204],[143,-1790],[1221,-65],[226,2141],[-1109,-621],[379,2173],[812,-788]],[[877937,714367],[-468,-2295],[903,1565],[-435,730]],[[328330,795571],[-1162,-87]],[[327168,795484],[-3740,1896]],[[323428,797380],[-886,1532]],[[322542,798912],[-1542,1007],[857,675]],[[321857,800594],[3536,-1399],[2892,-2500]],[[328285,796695],[45,-1124]],[[915797,775121],[-60,1171],[1897,2214],[568,-30],[-2405,-3355]],[[323107,780571],[1533,-828]],[[324640,779743],[2683,385]],[[327323,780128],[389,-389],[-2374,-2489]],[[325338,777250],[-485,1590]],[[324853,778840],[-622,-690]],[[324231,778150],[-1339,1448],[-978,163]],[[321914,779761],[-770,1278]],[[321144,781039],[1096,2492]],[[322240,783531],[-262,-1695]],[[321978,781836],[1129,-1265]],[[331391,775898],[539,2552]],[[331930,778450],[1778,-263]],[[333708,778187],[103,-1152]],[[333811,777035],[-1550,-1839]],[[332261,775196],[-2494,-479]],[[329767,774717],[-588,2178],[1737,5066]],[[330916,781961],[1283,1226]],[[332199,783187],[213,-1397],[-681,-3528],[-1340,-2777],[1000,413]],[[906131,768341],[1001,-340],[-1266,-1152],[-1459,-2374],[1724,3866]],[[899511,766086],[2706,-1047],[1512,2332],[-671,-3374],[666,-2736],[1367,494],[-911,-1254],[-2429,-1347],[-1837,-389],[-1500,-2739],[-536,-2481],[-2024,1527],[-1823,1903],[-1236,-191],[-1169,-1213],[-1404,1287],[-459,-1334],[2322,-3135],[-1364,62],[-1420,-2324],[-426,908],[314,1993],[-799,2812],[196,1550],[1653,2374],[-261,1500],[2511,-614],[281,2626],[686,2232],[382,4129],[-553,2604],[985,2095],[2129,-4091],[1623,-2502],[1489,-1657]],[[295648,774097],[-1094,-164]],[[294554,773933],[1345,1560],[-251,-1396]],[[912776,773199],[-1906,-1939],[-710,-78],[-1251,-2510],[-861,-884],[969,2677],[1775,2189],[1984,545]],[[9463,811999],[432,-667]],[[9895,811332],[-1456,-891]],[[8439,810441],[1024,1558]],[[6513,810872],[1645,1335],[-236,-1097],[-1409,-238]],[[13068,812920],[2748,1149]],[[15816,814069],[-104,-820],[-2644,-329]],[[33432,820758],[-3061,-3029]],[[30371,817729],[1969,3695]],[[32340,821424],[1032,595]],[[33372,822019],[60,-1261]],[[134017,819872],[828,-2926]],[[134845,816946],[-375,-732]],[[134470,816214],[1025,-2515],[-2620,3729],[58,1281],[-1119,819]],[[131814,819528],[2203,344]],[[142910,818356],[118,-2495]],[[143028,815861],[-470,-1356],[-187,2806]],[[142371,817311],[-426,-530],[-772,2038]],[[141173,818819],[401,1553],[1336,-2016]],[[34659,820350],[-795,281],[1868,1201]],[[35732,821832],[440,2586]],[[36172,824418],[1857,-1573],[-1281,-1312],[-2089,-1183]],[[139308,819707],[-1857,2230]],[[137451,821937],[1590,-639]],[[139041,821298],[267,-1591]],[[131512,825393],[1448,-552]],[[132960,824841],[1295,634]],[[134255,825475],[-953,-5192]],[[133302,820283],[-2316,1437]],[[130986,821720],[-577,1603]],[[130409,823323],[11,2256]],[[130420,825579],[1092,-186]],[[531559,829920],[1050,-499],[-783,-1059],[-1172,856],[905,702]],[[883167,831111],[738,-350],[-1514,-2253],[-551,1304],[318,1916],[1009,-617]],[[45900,830448],[327,-1453]],[[46227,828995],[-4071,-1875]],[[42156,827120],[-178,1117],[994,1619]],[[42972,829856],[1839,938]],[[44811,830794],[1089,-346]],[[962916,829609],[-14,-859],[-2483,3557],[1457,103],[1040,-2801]],[[541910,830692],[-1121,476],[225,1152],[896,-1628]],[[534913,835213],[-983,-1888],[-980,-4110],[-579,2453],[-1021,105],[-854,3064],[1378,1314],[958,-1456],[130,1602],[1978,569],[-27,-1653]],[[129708,833783],[-959,-1626]],[[128749,832157],[44,1600],[915,26]],[[529569,834175],[390,-2823],[-2213,177],[-356,2087],[2179,559]],[[136169,833460],[-581,-1676]],[[135588,831784],[-721,1199]],[[134867,832983],[-874,-1440],[-232,1485]],[[133761,833028],[614,2461]],[[134375,835489],[988,732]],[[135363,836221],[806,-2761]],[[482975,836075],[-490,-1917],[-389,1335],[879,582]],[[563676,853234],[-1059,-811],[-1350,1503],[1475,815],[934,-1507]],[[128983,838496],[1009,-115]],[[129992,838381],[2860,-4972]],[[132852,833409],[-1162,-96]],[[131690,833313],[1708,-1515],[-438,-2939]],[[132960,828859],[-1800,1990]],[[131160,830849],[-892,1846]],[[130268,832695],[195,1361],[-1797,1157]],[[128666,835213],[317,3283]],[[280734,838063],[-2959,-1979]],[[277775,836084],[2024,3959]],[[279799,840043],[935,-1980]],[[132963,836150],[-1464,799]],[[131499,836949],[779,2491]],[[132278,839440],[868,-1507],[-183,-1783]],[[483950,838526],[-1108,-330],[205,2116],[903,-1786]],[[127916,837243],[-665,-301]],[[127251,836942],[-512,4513]],[[126739,841455],[641,555],[1123,-1671],[-587,-3096]],[[76620,850469],[855,-1179],[-1868,-861]],[[75607,848429],[-1666,423],[1500,1950]],[[75441,850802],[1179,-333]],[[552990,847363],[-730,-768],[261,-1825],[-2115,-2831],[-27,3770],[1113,1623],[1498,31]],[[75283,847292],[1304,11]],[[76587,847303],[590,-1474]],[[77177,845829],[-2940,-2078]],[[74237,843751],[-1341,-2179],[-1352,1686]],[[71544,843258],[-47,-1370]],[[71497,841888],[-1237,2510]],[[70260,844398],[475,1327]],[[70735,845725],[1404,422]],[[72139,846147],[349,1121]],[[72488,847268],[1055,-526],[1011,1426]],[[74554,848168],[729,-876]],[[545912,838208],[-323,1649],[1703,4598],[257,-149],[-1637,-6098]],[[125084,844493],[969,-3752]],[[126053,840741],[-93,-2907]],[[125960,837834],[-614,2625],[-1265,896],[363,1217]],[[124444,842572],[-838,1284],[-863,-1390]],[[122743,842466],[69,1825]],[[122812,844291],[1226,1278],[1046,-1076]],[[129538,842431],[1145,-729]],[[130683,841702],[-719,-2463]],[[129964,839239],[-1084,-3]],[[128880,839236],[-1046,3232]],[[127834,842468],[1704,-37]],[[482931,845403],[1218,-1399],[-676,-1326],[-2254,2354],[1428,1237],[284,-866]],[[125887,849293],[1223,-106],[-74,-1536]],[[127036,847651],[948,-3245]],[[127984,844406],[-1849,-1451],[291,2312],[-1170,4625],[631,-599]],[[123296,848287],[1697,351]],[[124993,848638],[197,-3377]],[[125190,845261],[-1573,1073],[-539,-1435],[-2435,3271],[685,1462],[1486,294],[482,-1639]],[[482781,850488],[-630,-2027],[-1476,-1606],[-153,2835],[2151,1624],[108,-826]],[[954541,851909],[350,2440],[2254,1221],[120,-1988],[-2724,-1673]],[[562826,852016],[1876,-816],[-1563,-1498],[-1507,-452],[-904,2031],[2098,735]],[[891694,943123],[-2524,1095],[2077,531],[447,-1626]],[[814964,945499],[-1680,-1810],[-3553,1529],[1614,1161],[3619,-880]],[[907764,951248],[4547,253],[119,-836],[4524,-315],[1507,-1627],[-2915,-1019],[-4179,314],[-5400,2207],[1797,1023]],[[210778,949266],[-2132,660]],[[208646,949926],[1503,1671]],[[210149,951597],[1956,-1581]],[[212105,950016],[-1327,-750]],[[240159,949216],[-12,-1995]],[[240147,947221],[-3196,-291]],[[236951,946930],[-5173,2064],[2469,3189]],[[234247,952183],[3455,383]],[[237702,952566],[2457,-3350]],[[449998,951464],[1151,-2456],[-3013,53],[-514,1881],[2376,522]],[[183799,965371],[-3325,1261]],[[180474,966632],[1941,652]],[[182415,967284],[1384,-1913]],[[250463,962485],[-3651,709],[-5,1308]],[[246807,964502],[2715,-79]],[[249522,964423],[941,-1938]],[[560022,970354],[4190,-3580],[4959,-1392],[-5833,-2848],[-192,1561],[-4675,-583],[1676,2859],[-3585,3503],[3460,480]],[[193893,964006],[-4872,-1067]],[[189021,962939],[-3367,1103],[-63,2262]],[[185591,966304],[5502,1043],[4304,-54],[-3358,-1451],[1854,-1836]],[[209605,962901],[-1869,-922],[-2364,3218],[4233,-2296]],[[234765,965592],[5282,-126]],[[240047,965466],[176,-1756],[-6854,57]],[[233369,963767],[1396,1825]],[[200410,955317],[-468,-1498]],[[199942,953819],[3136,-133]],[[203078,953686],[1376,1646]],[[204454,955332],[2543,-1863]],[[206997,953469],[-1060,-3283]],[[205937,950186],[-3586,-1567],[-4660,816]],[[197691,949435],[-5861,-2524],[-4385,-1315]],[[187445,945596],[-2762,79]],[[184683,945675],[-1718,1990]],[[182965,947665],[3602,1240],[3235,261]],[[189802,949166],[1770,1228],[-7438,-937]],[[184134,949457],[-847,2167]],[[183287,951624],[-1209,-2052]],[[182078,949572],[-3547,-710]],[[178531,948862],[-5198,1799]],[[173333,950661],[1238,1192]],[[174571,951853],[2992,119]],[[177563,951972],[2654,1261],[-5287,-618]],[[174930,952615],[1909,1655]],[[176839,954270],[-755,1141],[2858,2156]],[[178942,957567],[3852,83]],[[182794,957650],[1029,-1449]],[[183823,956201],[3128,-31],[4569,-3869]],[[191520,952301],[5461,-249],[-1970,2112]],[[195011,954164],[1016,1458]],[[196027,955622],[-2333,1824],[2588,2032]],[[196282,959478],[2421,-133],[-127,-1769]],[[198576,957576],[1834,-2259]],[[889023,953962],[2131,-1174],[1859,3000],[2709,-1385],[3404,-235],[4359,-1648],[-1033,-1876],[-2398,-1329],[-3584,1736],[758,1909],[-2545,5],[1474,-3125],[1420,-965],[-1820,-888],[-1349,1012],[-4776,-855],[-1839,585],[-1407,-1713],[-2797,835],[-2429,1933],[141,3707],[4586,2650],[3136,-2179]],[[877634,951478],[-1379,-120],[685,2700],[694,-2580]],[[238068,960381],[3611,-1730]],[[241679,958651],[4696,358],[5273,-2913]],[[251648,956096],[-5202,-173]],[[246446,955923],[5762,-2357],[-1225,-1167],[2026,-659]],[[253009,951740],[4609,971],[3302,-685]],[[260920,952026],[5935,1877]],[[266855,953903],[4940,71]],[[271795,953974],[5088,-1196]],[[276883,952778],[1910,-2546]],[[278793,950232],[-2009,-875],[2656,-794]],[[279440,948563],[-2434,-1991]],[[277006,946572],[-4253,-622]],[[272753,945950],[-9035,772]],[[263718,946722],[-425,-523],[-8913,-145]],[[254380,946054],[231,1722]],[[254611,947776],[-4179,-1399],[-5881,1449]],[[244551,947826],[-1293,3277]],[[243258,951103],[995,1846],[-2842,4124],[-6062,-532]],[[235349,956541],[-4365,2738]],[[230984,959279],[243,1454]],[[231227,960733],[3111,545]],[[234338,961278],[3730,-897]],[[168348,952708],[4562,2934],[350,-868],[-2743,-2669],[-2169,603]],[[228608,957739],[1014,-6202]],[[229622,951537],[-940,-1733]],[[228682,949804],[-2484,-698]],[[226198,949106],[-4627,-9]],[[221571,949097],[-1380,2007]],[[220191,951104],[3167,1830]],[[223358,952934],[-8195,-840]],[[215163,952094],[1662,2193]],[[216825,954287],[235,3289]],[[217060,957576],[5106,-2959]],[[222166,954617],[-2419,3175]],[[219747,957792],[6056,1294]],[[225803,959086],[2805,-1347]],[[216034,955063],[-3020,-1484],[-2878,2477]],[[210136,956056],[4908,588]],[[215044,956644],[990,-1581]],[[211047,958429],[2699,-788]],[[213746,957641],[-3387,-733]],[[210359,956908],[688,1521]],[[448381,955226],[-1397,2299],[981,1254],[416,-3553]],[[179024,963052],[-2040,-1550]],[[176984,961502],[181,-2906],[-2593,-1855]],[[174572,956741],[-2406,880]],[[172166,957621],[666,2001]],[[172832,959622],[-2809,-1607]],[[170023,958015],[-1046,-2290],[-989,1266],[-1080,-2852]],[[166908,954139],[-3074,957]],[[163834,955096],[-3836,-451]],[[159998,954645],[97,2707],[2089,239],[7010,5116]],[[169194,962707],[5031,50],[2544,1359]],[[176769,964116],[2255,-1064]],[[546629,978121],[3249,-1200],[3148,-3242],[2981,-67],[3406,-2402],[-4500,-696],[-3693,-3541],[-4830,-8566],[-6149,3672],[-1155,2163],[8228,1383],[-501,675],[-6247,-859],[-2565,1550],[8601,1910],[16,1855],[-3792,-1128],[-263,1824],[-2377,-626],[475,-1545],[-4133,-1051],[-3823,2839],[1391,1114],[-1930,2245],[-1033,-911],[-950,3951],[6037,-660],[2550,-2047],[1767,2719],[4725,-4843],[-1329,4150],[2696,1334]],[[306975,996546],[8048,-431]],[[315023,996115],[-2237,-1635]],[[312786,994480],[6922,1379],[5055,-1988]],[[324763,993871],[4467,-581]],[[329230,993290],[-1943,-2511]],[[327287,990779],[-6660,-1834],[-5699,-695]],[[314928,988250],[-4700,-2105]],[[310228,986145],[7173,1381]],[[317401,987526],[699,-1242]],[[318100,986284],[-8741,-3591]],[[309359,982693],[-7659,-5431],[-5790,-32],[17,-1548]],[[295927,975682],[-4981,-439]],[[290946,975243],[-4554,541]],[[286392,975784],[6573,-2724],[-11248,133]],[[281717,973193],[1942,-786]],[[283659,972407],[4519,382]],[[288178,972789],[5063,-1675]],[[293241,971114],[-1237,-1062]],[[292004,970052],[-4153,-140],[3278,-1146]],[[291129,968766],[-1868,-1883],[-5963,-378]],[[283298,966505],[-177,-2530]],[[283121,963975],[-2203,-1105]],[[280918,962870],[-6244,-373]],[[274674,962497],[4934,-659]],[[279608,961838],[334,-1317]],[[279942,960521],[2589,247]],[[282531,960768],[12,-2408],[-6683,-2339]],[[275860,956021],[-1334,1992]],[[274526,958013],[-3776,1247],[824,-1525]],[[271574,957735],[-4590,-75]],[[266984,957660],[-3488,-880]],[[263496,956780],[-2707,772]],[[260789,957552],[-6334,-177]],[[254455,957375],[-3261,515]],[[251194,957890],[195,1984],[3060,1641]],[[254449,961515],[4295,418],[-3453,3228]],[[255291,965161],[2992,1025]],[[258283,966186],[3971,-2555]],[[262254,963631],[2361,-592]],[[264615,963039],[2664,1016]],[[267279,964055],[-4194,157]],[[263085,964212],[-717,2184]],[[262368,966396],[1453,2279]],[[263821,968675],[-3315,-1370]],[[260506,967305],[827,1550]],[[261333,968855],[-4533,-984],[2067,3540]],[[258867,971411],[5011,818]],[[263878,972229],[4812,-841]],[[268690,971388],[2313,790]],[[271003,972178],[-3158,889],[-4206,3309],[-3697,1380]],[[259942,977756],[316,2809]],[[260258,980565],[7176,-535],[5189,-3001]],[[272623,977029],[2349,-174],[-5420,3465]],[[269552,980320],[8084,1485]],[[277636,981805],[8891,2071]],[[286527,983876],[-4724,256],[733,1459],[-7556,-3038]],[[274980,982553],[-8525,-584],[-9038,672]],[[257417,982641],[-4421,805]],[[252996,983446],[1412,1150]],[[254408,984596],[-4262,1025],[4079,2322]],[[254225,987943],[-8122,80]],[[246103,988023],[2535,1771]],[[248638,989794],[7920,1232]],[[256558,991026],[7206,-606]],[[263764,990420],[-4267,1210],[4678,1554]],[[264175,993184],[15200,-3524]],[[279375,989660],[-8407,3393]],[[270968,993053],[4004,2085],[6281,-591]],[[281253,994547],[-3906,1373]],[[277347,995920],[20398,1008],[9230,-382]],[[38512,862457],[1128,-411],[383,-2376]],[[40023,859670],[-1431,-816],[-2868,1381]],[[35724,860235],[-825,1173]],[[34899,861408],[1666,62]],[[36565,861470],[1947,987]],[[89622,859078],[1542,3229]],[[91164,862307],[539,-616]],[[91703,861691],[-2081,-2613]],[[319909,868276],[-1665,1681]],[[318244,869957],[1784,75],[-119,-1756]],[[291239,908966],[74,-4127]],[[291313,904839],[-1814,-1504],[-3402,-98]],[[286097,903237],[-836,2602],[1571,3111]],[[286832,908950],[2427,625],[1980,-609]],[[295495,906299],[-2644,266],[-369,1412],[3531,-575]],[[296013,907402],[-518,-1103]],[[639625,914604],[-1775,-1932],[-2665,-750],[-1078,1820],[979,2346],[1650,444],[2889,-1928]],[[543778,910905],[1580,1867],[528,-1442],[-1685,-1444],[-4597,-1176],[3208,2518],[196,2533],[1138,1390],[-368,-4246]],[[542241,913167],[42,-1936],[-1948,99],[1906,1837]],[[279970,912589],[-542,459]],[[279428,913048],[2248,2652]],[[281676,915700],[1021,-396]],[[282697,915304],[-2727,-2715]],[[259456,906015],[-1011,2159]],[[258445,908174],[1496,493]],[[259941,908667],[-485,-2652]],[[291997,909646],[-1442,1047],[1157,724]],[[291712,911417],[285,-1771]],[[304619,875284],[-1192,285]],[[303427,875569],[-1024,1666]],[[302403,877235],[1923,-856]],[[304326,876379],[293,-1095]],[[23714,881749],[2868,349]],[[26582,882098],[2786,-2077],[1975,-223]],[[31343,879798],[-377,-826]],[[30966,878972],[-2572,-459]],[[28394,878513],[-2080,1691]],[[26314,880204],[-3512,269]],[[22802,880473],[912,1276]],[[284615,879658],[-1122,-1024]],[[283493,878634],[-1569,1996]],[[281924,880630],[2232,-120],[459,-852]],[[285098,881443],[641,554],[1267,-1706],[-1908,1152]],[[264112,891353],[853,1104],[7118,-4758]],[[272083,887699],[1039,-2557],[-630,-1075]],[[272492,884067],[2983,348]],[[275475,884415],[1463,-1942]],[[276938,882473],[-2067,-1781]],[[274871,880692],[-3699,1453]],[[271172,882145],[-248,1304]],[[270924,883449],[-2853,1020]],[[268071,884469],[-650,-1693]],[[267421,882776],[-2513,-2986]],[[264908,879790],[-2396,-1008]],[[262512,878782],[-860,3361],[-2894,-777]],[[258758,881366],[-950,574]],[[257808,881940],[2603,2753],[-340,2541]],[[260071,887234],[1146,6745]],[[261217,893979],[1131,1269]],[[262348,895248],[1764,-3895]],[[456824,897085],[1909,906],[-723,-1653],[769,-1905],[3246,-1371],[250,-2408],[-2684,-4130],[-4204,-1983],[-1611,-1456],[-3266,-903],[-2327,-1815],[-4289,883],[-2913,2249],[-3778,-581],[-379,1079],[1907,308],[1139,1934],[-2279,2353],[-4331,405],[5928,1098],[-1615,1054],[1748,1308],[-2939,788],[-2771,-1024],[-1542,550],[953,1536],[2296,-57],[-1255,1892],[1995,-517],[1593,521],[-1873,1701],[1927,432],[2831,-2396],[-70,-3268],[840,-1229],[1083,2319],[792,-516],[272,2739],[2485,-1546],[220,1797],[1681,552],[1885,-2007],[-550,1940],[2074,-1144],[1103,1413],[1125,-422],[577,1867],[1543,402],[1228,-1695]],[[481580,873383],[-254,-1407],[-1249,1749],[1503,-342]],[[279041,874472],[614,-2284]],[[279655,872188],[-957,-2261],[-1657,1029]],[[277041,870956],[677,3109]],[[277718,874065],[1323,407]],[[272221,877686],[-315,-1789]],[[271906,875897],[-2506,-2620]],[[269400,873277],[-1897,-294]],[[267503,872983],[-173,848]],[[267330,873831],[-420,723]],[[266910,874554],[36,302]],[[266946,874856],[1453,2538]],[[268399,877394],[3822,292]],[[0,890205],[0,1449]],[[0,891654],[0,21752],[3127,-1359],[505,-1232],[9297,-5142],[1377,-1952],[-210,-4298],[1476,-1654],[1079,2575],[-1228,1921],[2133,247],[1253,-1043],[3980,-218],[4310,-4517],[1295,-155],[-2118,-1646],[-349,-1445],[-2180,1024],[1101,-1448],[-2368,-319],[-2434,1096],[1587,-1516],[-3,-2233],[-2184,-1017],[-28,-3431],[-2755,898],[-880,1118],[-2993,976],[-1273,1235],[-666,2726],[-2674,845],[-3483,-763],[-2214,5101],[-1880,-1943],[1199,-2969],[-1799,-2663]],[[264792,893213],[-1282,1457]],[[263510,894670],[549,1112]],[[264059,895782],[733,-2569]],[[267428,894527],[-1090,-148]],[[266338,894379],[-803,2127],[1893,-1979]],[[259474,925417],[4151,836]],[[263625,926253],[624,-889]],[[264249,925364],[584,3462]],[[264833,928826],[-3477,2372]],[[261356,931198],[1405,1352],[2929,-960],[-2749,2184]],[[262941,933774],[-843,2092],[1739,3324],[3435,482]],[[267272,939672],[3118,1852],[3482,-563],[3142,-5266],[-2651,-2571]],[[274363,933124],[1030,-2371],[2268,2860]],[[277661,933613],[3730,-253],[2703,-1016]],[[284094,932344],[-1734,2489],[4047,714]],[[286407,935547],[4442,-1421]],[[290849,934126],[1086,-2253]],[[291935,931873],[1695,-297]],[[293630,931576],[-308,-2239]],[[293322,929337],[4172,32],[4572,-1873]],[[302066,927496],[-664,-1520],[1952,-12]],[[303354,925964],[381,-1376]],[[303735,924588],[-1724,-2195]],[[302011,922393],[3684,2042]],[[305695,924435],[4421,-1908]],[[310116,922527],[-1557,-1872]],[[308559,920655],[485,-1573],[1732,2211]],[[310776,921293],[2102,-1660]],[[312878,919633],[395,-1800]],[[313273,917833],[-2479,139]],[[310794,917972],[-1108,-1048]],[[309686,916924],[4839,-1425]],[[314525,915499],[-89,-1090]],[[314436,914409],[-3154,565]],[[311282,914974],[518,-1241]],[[311800,913733],[-795,-2890]],[[311005,910843],[3678,-624]],[[314683,910219],[-325,-1362]],[[314358,908857],[1718,384],[-559,-2228],[3991,824]],[[319508,907837],[2280,-2491]],[[321788,905346],[889,-2126]],[[322677,903220],[2211,-172],[-1837,-2445]],[[323051,900603],[4814,1165],[1858,-2195]],[[329723,899573],[-1564,-1989]],[[328159,897584],[-1918,557]],[[326241,898141],[1560,-2201]],[[327801,895940],[-1663,-5]],[[326138,895935],[-191,-2337]],[[325947,893598],[-1885,-138],[-747,-4080]],[[323315,889380],[-803,1074]],[[322512,890454],[-1832,43]],[[320680,890497],[-2351,3836]],[[318329,894333],[1103,1858]],[[319432,896191],[-2282,-478]],[[317150,895713],[-2671,3310]],[[314479,899023],[-1444,83],[-11,-1576],[-1548,1105]],[[311476,898635],[1904,-2700]],[[313380,895935],[-2983,-568],[3164,-2952],[-578,-496]],[[312983,891919],[1831,-2268],[2023,-521]],[[316837,889130],[2400,-2661]],[[319237,886469],[-265,-2421]],[[318972,884048],[1365,0]],[[320337,884048],[456,-4527],[-1882,2964]],[[318911,882485],[396,-3137]],[[319307,879348],[1016,-1969]],[[320323,877379],[-1331,179]],[[318992,877558],[313,-1697]],[[319305,875861],[-3261,2732],[-529,-474]],[[315515,878119],[-2126,1645]],[[313389,879764],[-1982,2540]],[[311407,882304],[474,-1842]],[[311881,880462],[-2142,1793],[-1159,-131]],[[308580,882124],[2138,-3146],[1293,-466]],[[312011,878512],[4710,-5241]],[[316721,873271],[-768,-2018]],[[315953,871253],[-3287,1676]],[[312666,872929],[-2607,497],[-1955,1008]],[[308104,874434],[-1285,2010],[-1920,112]],[[304899,876556],[-2826,1654],[-1998,2293],[1436,489],[-2131,1165]],[[299380,882157],[-3390,4234]],[[295990,886391],[-4091,2183]],[[291899,888574],[767,-1392],[-6156,-1892],[-2597,767]],[[283913,886057],[-1065,1485]],[[282848,887542],[219,1905]],[[283067,889447],[2041,1524]],[[285108,890971],[95,1520]],[[285203,892491],[4162,-1339],[333,524]],[[289698,891676],[5994,1005]],[[295692,892681],[-543,1668],[-1910,2206],[3424,3318],[2726,3289]],[[299389,903162],[-3020,6597]],[[296369,909759],[-937,-434]],[[295432,909325],[-894,1684],[-1267,6],[-1375,2388],[-4128,-1721]],[[287768,911682],[-427,1878]],[[287341,913560],[1676,127]],[[289017,913687],[463,1704]],[[289480,915391],[-1725,727]],[[287755,916118],[-292,1438]],[[287463,917556],[-2996,958]],[[284467,918514],[-697,2378]],[[283770,920892],[-1223,-106]],[[282547,920786],[-2176,2219],[-781,-1370],[1272,-2340],[-2018,-490]],[[278844,918805],[-5399,1283]],[[273445,920088],[-1608,-1600]],[[271837,918488],[-2646,964]],[[269191,919452],[-2133,-244]],[[267058,919208],[-6842,1081]],[[260216,920289],[-839,1516]],[[259377,921805],[-3546,-884]],[[255831,920921],[-2632,1606]],[[253199,922527],[-1688,3192]],[[251511,925719],[4475,-695]],[[255986,925024],[1957,398]],[[257943,925422],[-2033,1167],[-3353,470]],[[252557,927059],[-2129,1211]],[[250428,928270],[-498,2703]],[[249930,930973],[453,2745]],[[250383,933718],[1662,3893],[4288,3874]],[[256333,941485],[2641,658]],[[258974,942143],[4608,-153]],[[263582,941990],[377,-672],[-3088,-2574],[-1508,-2308]],[[259363,936436],[1647,-6515]],[[261010,929921],[2814,-2475]],[[263824,927446],[-4350,-2029]],[[301559,899263],[867,-1573],[-276,-2234],[1790,1987],[2847,-459],[185,2188],[-2702,1022],[-1976,1627],[-735,-2558]],[[303757,887946],[-344,2276],[-2683,2000],[626,-2975],[-1076,-1051],[1111,-705],[1617,1086],[749,-631]],[[496366,863369],[710,-551],[-621,-1955],[-1077,995],[988,1511]],[[310717,863514],[897,-667]],[[311614,862847],[-1518,-1158],[621,1825]],[[996837,924325],[82,2397],[3080,1817],[0,-3227],[-3162,-987]],[[0,925312],[0,1133]],[[0,926445],[0,1276]],[[0,927721],[0,818]],[[0,928539],[4572,-51],[2283,-1576],[-804,-1158],[-4681,-855],[-1370,413]],[[970001,916943],[-3922,1519],[1581,1060],[2825,-789],[-484,-1790]],[[565112,924262],[-3005,-1783],[-1012,843],[4017,940]],[[429354,924887],[84,-1591],[-2421,-1176],[-919,587],[-3593,-589],[525,2626],[2036,-204],[3214,1072],[1074,-725]],[[948519,912918],[-918,1240],[595,1534],[323,-2774]],[[232499,915545],[1985,-3018]],[[234484,912527],[-2266,-2158]],[[232218,910369],[-2974,431]],[[229244,910800],[-2357,1773],[-3112,444]],[[223775,913017],[-41,1265],[2777,1204]],[[226511,915486],[522,2488],[1326,635]],[[228359,918609],[4140,-3064]],[[286124,914356],[-898,1615]],[[285226,915971],[1805,-297],[-907,-1318]],[[548619,917037],[1392,-541],[-149,-1818],[-2193,-1020],[-442,1990],[1392,1389]],[[277839,916524],[-2223,991]],[[275616,917515],[3289,791],[-1066,-1782]],[[358295,916778],[-935,1790],[1865,-37],[-930,-1753]],[[353524,919101],[1905,-814],[-187,-1885],[-4071,-1377],[-685,1680],[-3040,1027],[1658,1353],[-1370,1466],[1233,757],[2768,-568],[1789,-1639]],[[553486,919822],[936,-570],[-2243,-2318],[-1209,1120],[2398,2841],[118,-1073]],[[667917,919042],[-28,-1237],[-4132,989],[-1333,2215],[1479,1176],[4014,-3143]],[[475129,924399],[1694,1784],[948,-586],[-2642,-1198]],[[647614,926786],[-329,-1618],[-2148,1873],[2477,-255]],[[720837,935555],[-2348,1009],[1411,1197],[937,-2206]],[[715645,933003],[-2018,38],[1869,1974],[2184,-880],[-2035,-1132]],[[347175,935965],[-1408,-1320],[-1920,892],[3328,428]],[[223930,942411],[4686,1360],[-1247,-1303],[-3439,-57]],[[182061,934716],[2661,676]],[[184722,935392],[811,1698]],[[185533,937090],[5384,-1584]],[[190917,935506],[-1737,-2119]],[[189180,933387],[2097,55],[2596,1753]],[[193873,935195],[-1264,2056],[1812,-146],[3639,-2869],[1355,-4433]],[[199415,929803],[2513,851],[-1083,1508]],[[200845,932162],[-1507,5667]],[[199338,937829],[581,1439]],[[199919,939268],[2995,-431],[3162,-1572]],[[206076,937265],[4064,-9341]],[[210140,927924],[-611,-1954]],[[209529,925970],[1711,-2023]],[[211240,923947],[2511,-637],[4131,-3081],[1444,-144]],[[219326,920085],[298,-2344],[-3608,753]],[[216016,918494],[-1075,-1723]],[[214941,916771],[-2050,794]],[[212891,917565],[747,-2805]],[[213638,914760],[1787,1566],[1309,-410],[329,-2270],[-2883,-1187]],[[214180,912459],[-6141,574]],[[208039,913033],[240,953]],[[208279,913986],[-3115,478]],[[205164,914464],[-1439,1645],[-2169,-2592]],[[201556,913517],[-4184,-1436],[-6569,-1290]],[[190803,910791],[-5047,-284]],[[185756,910507],[-1359,2040]],[[184397,912547],[-214,2113]],[[184183,914660],[-4069,413]],[[180114,915073],[-3763,947]],[[176351,916020],[-1640,2248],[-87,1754]],[[174624,920022],[7064,1258],[5428,-517]],[[187116,920763],[2793,495]],[[189909,921258],[-5902,2263]],[[184007,923521],[-6204,-619]],[[177803,922902],[-4434,256]],[[173369,923158],[-1880,1534],[1250,1600],[5340,1323]],[[178079,927615],[846,975]],[[178925,928590],[-5934,-923]],[[172991,927667],[-54,1592],[-3127,163]],[[169810,929422],[-213,1770]],[[169597,931192],[2032,1642]],[[171629,932834],[-448,1607]],[[171181,934441],[2286,1760]],[[173467,936201],[8093,3209]],[[181560,939410],[1318,-609]],[[182878,938801],[152,-2422]],[[183030,936379],[-969,-1663]],[[167398,943794],[1680,-502]],[[169078,943292],[1633,1284]],[[170711,944576],[2859,-77]],[[173570,944499],[5567,-3631]],[[179137,940868],[177,-1066]],[[179314,939802],[-9763,-4471]],[[169551,935331],[-1239,-1918],[-2145,-876],[-1221,-4188]],[[164946,928349],[-3139,-361]],[[161807,927988],[-3299,-2114],[-2974,3493]],[[155534,929367],[-4875,2725]],[[150659,932092],[2154,2668]],[[152813,934760],[419,2893]],[[153232,937653],[2887,4099]],[[156119,941752],[-2498,3437]],[[153621,945189],[9391,1077]],[[163012,946266],[4869,-1760]],[[167881,944506],[-483,-712]],[[222216,942806],[2345,-1271]],[[224561,941535],[4378,925]],[[228939,942460],[1612,-1309],[-753,-1657]],[[229798,939494],[-3234,-2290]],[[226564,937204],[2670,-48]],[[229234,937156],[880,-2070]],[[230114,935086],[1713,331]],[[231827,935417],[-705,-2280]],[[231122,933137],[507,-2844]],[[231629,930293],[-2691,-1209]],[[228938,929084],[-2435,850]],[[226503,929934],[723,-1969]],[[227226,927965],[-2690,-436],[-3965,4650]],[[220571,932179],[-3138,964],[-2749,2773]],[[214684,935916],[2198,1623]],[[216882,937539],[1588,-1840]],[[218470,935699],[2405,158]],[[220875,935857],[1078,1127]],[[221953,936984],[-951,1726]],[[221002,938710],[-2810,1045]],[[218192,939755],[1488,2218]],[[219680,941973],[2536,833]],[[416796,999793],[11551,-1800],[-17216,-1041],[13863,-107],[5218,547],[1814,-1672],[8193,-1670],[-6503,-1827],[-15882,-746],[-643,-1219],[12951,271],[2377,-1778],[3304,1841],[4904,337],[532,-2213],[-5350,-4552],[3170,731],[5321,3046],[7111,-987],[5278,2582],[9341,-1093],[1845,-1333],[-8263,-3915],[-6270,-1125],[2287,-863],[-2586,-1359],[-7113,351],[-1971,-2692],[2375,-711],[580,-3145],[-5225,-3538],[-2199,-4529],[2458,718],[3811,-1143],[-3306,-592],[1247,-1484],[5257,-908],[-475,-2589],[-6755,644],[-2598,-1858],[1279,-1832],[3648,-268],[1651,-2733],[232,-3126],[-2942,500],[-3608,-2031],[2378,485],[879,-1926],[1739,1463],[2112,-2937],[-401,-1158],[-4890,-1025],[-3346,1038],[-3,-1320],[5467,-1275],[-396,-2105],[-4654,-1321],[-2230,452],[-3249,2478],[-1760,-1500],[-5450,-2312],[6017,1789],[1912,-146],[5174,-2843],[-714,-4733],[-4933,2246],[-1558,3193],[-5632,-1907],[5240,906],[290,-2555],[7519,-4104],[-1509,-1447],[2086,-132],[637,-5640],[-3244,-526],[-3059,698],[-2140,3960],[-3676,2064],[-3393,-233],[3748,-549],[43,-1519],[-2710,-1382],[-4404,337],[649,-1826],[-1310,-1317],[5647,-474],[-2924,-1613],[5642,1355],[6546,-1414],[2469,67],[-2028,-1901],[-6039,-3225],[267,-565],[-3469,-2743],[-8078,-2390],[-1720,76],[-2062,-1148],[-2246,63],[-2522,1829],[453,-2643],[-2757,-2160],[-2624,-5335],[-3019,-2818],[-1886,1131],[658,-1785],[-2081,-1831],[-1898,240],[-1920,-1649],[-686,691],[1972,3639],[-1281,-370],[-2458,-3774],[-4318,-605],[1705,-1076],[-1877,-1730],[-2308,309],[2506,-3679],[-1665,-1529],[642,-2942],[-1384,-1252],[-163,-1422],[-2641,-3435],[-2454,155],[2191,-899],[-468,-2463],[587,-1751],[-593,-1039],[-1093,-5417],[-1607,-1911],[480,-2273],[-2762,-1358],[-934,1081],[-2572,1116],[-3,1434],[-1851,1012],[548,3349],[-2848,-2160],[-3696,234],[-1991,2497],[-1273,3631],[1532,1122],[-2208,-480],[195,1387],[-2127,1425],[-197,2066],[-2996,4860],[-664,3334],[1323,2105],[3083,849],[-1667,556],[-1887,-1033],[-1449,-2396],[-474,1168],[-708,6194],[-2363,785],[257,2271],[-794,421],[3711,2719],[2153,2273],[-4558,-3851],[-1924,-512],[-56,1536],[1657,2447],[-2350,1829],[238,1675],[3145,1964],[4125,-671],[591,1008],[-3823,180],[-3862,-1706],[1621,3903],[4318,-907],[1083,1603],[-3253,-633],[-2791,468],[955,1857],[2046,535],[1450,-906],[1476,1135],[1470,5839],[1190,1712],[-5452,264],[-2135,1439],[-2753,710],[-1435,1645],[1014,716],[3789,-413],[3548,-1843],[411,4026],[-4531,361],[717,1905],[-1918,459],[-104,1606],[-1349,-2317],[-3815,-190],[-920,1184],[978,2866],[-795,2032],[2254,1115],[146,1368],[-2652,1424],[1098,885],[-2241,1752],[485,1999],[-2158,1918],[1252,1823],[-6522,5086],[243,1799],[-7941,2908],[-5733,946],[-4092,-989],[-5953,-123],[1056,-1033],[-4094,531],[-3710,1967],[3502,1650],[-6161,769],[-1171,2180],[5235,118],[1074,872],[6118,-369],[-839,2375],[-7389,-1268],[-7538,2783],[-2092,1526],[1174,1835],[9450,2092],[4198,1538],[4207,92],[1522,1232],[2431,4719],[-3516,-668],[-3697,843],[404,1461],[8623,3812],[2314,-1012],[431,1970],[4076,-501],[557,3324],[5483,1849],[12599,2042],[3178,-1718],[2240,1542],[5225,-2527],[3759,136],[-4024,3211],[5913,-324],[9917,-3416],[2107,119],[816,3076],[-3649,2116],[11439,316],[-9982,634],[-1074,781],[6681,1464],[4748,-970],[2620,1371],[5774,-1975],[1251,2883],[21874,470]],[[653666,939029],[3082,-635],[-960,-2440],[-2022,-1922],[-162,-3137],[2071,-3494],[2366,-2480],[2029,-1174],[-1332,-828],[-7069,539],[-3382,1145],[2145,1494],[-2200,2464],[-4309,-297],[-1038,1691],[399,1744],[1860,347],[3105,4663],[-478,1367],[4461,1423],[1434,-470]],[[202996,940045],[854,1278]],[[203850,941323],[3059,416],[2400,-897]],[[209309,940842],[183,-1543]],[[209492,939299],[-2102,-2601],[-4394,3347]],[[894957,942510],[3218,-1938],[410,-1911],[-5262,383],[-3378,1020],[1956,2267],[3056,179]],[[241192,944080],[2634,-1117]],[[243826,942963],[3152,218],[2037,-834]],[[249015,942347],[-4898,-6603],[-5815,18]],[[238302,935762],[1822,-1989]],[[240124,933773],[-1340,-2325]],[[238784,931448],[-3209,-8]],[[235575,931440],[-985,4468]],[[234590,935908],[-237,5414]],[[234353,941322],[2598,-189]],[[236951,941133],[-951,2135]],[[236000,943268],[5192,812]],[[279063,941080],[3474,67]],[[282537,941147],[3000,-985]],[[285537,940162],[2547,-2480]],[[288084,937682],[-308,-1542],[-3987,451]],[[283789,936591],[-4624,-835]],[[279165,935756],[-1897,2777]],[[277268,938533],[-1780,924]],[[275488,939457],[171,2234],[3404,-611]],[[696316,937765],[-2094,-63],[637,2135],[2197,413],[1905,-2017],[-2645,-468]],[[688236,956383],[-4119,-1504],[-13684,-3964],[-2374,-2429],[-3890,-2353],[-1573,-51],[-259,-2192],[-4105,-4516],[-1482,-411],[-3954,928],[-1964,-611],[-1491,2461],[2444,1146],[2609,3958],[2546,1951],[1636,2529],[1442,-252],[3540,3042],[5725,1283],[721,1248],[4916,-268],[8032,2230],[4644,2338],[2641,-439],[1151,-2138],[-3152,-1986]],[[933113,802729],[-1883,-1229],[-69,1204],[1283,609],[1157,2200],[-488,-2784]],[[146674,804734],[4765,-1918],[2331,-5262],[1797,-1212]],[[155567,796342],[1386,-3803],[-272,-1472]],[[156681,791067],[-3041,1562],[-1198,969]],[[152442,793598],[753,1585]],[[153195,795183],[-1696,-517]],[[151499,794666],[-511,1450]],[[150988,796116],[-1665,1522],[-289,1292]],[[149034,798930],[-2506,2827]],[[146528,801757],[-1706,-61]],[[144822,801696],[-116,1881]],[[144706,803577],[1165,-240],[57,1057]],[[145928,804394],[-1647,-501]],[[144281,803893],[-798,1456]],[[143483,805349],[1189,689]],[[144672,806038],[2002,-1304]],[[345948,810042],[-1590,-1232],[443,-2495],[-2285,-5023]],[[342516,801292],[-356,-2642]],[[342160,798650],[1598,2823]],[[343758,801473],[238,-888]],[[343996,800585],[1754,338]],[[345750,800923],[-1417,-1733]],[[344333,799190],[-131,-1497],[2445,178]],[[346647,797871],[-104,-1672]],[[346543,796199],[2153,1955],[16,-1114],[1405,593],[940,-712]],[[351057,796921],[128,-1069]],[[351185,795852],[-1091,-2574],[744,-160]],[[350838,793118],[-1027,-1546]],[[349811,791572],[2807,1423]],[[352618,792995],[-218,-1523],[-1316,-1152]],[[351084,790320],[-700,-2418]],[[350384,787902],[716,-812]],[[351100,787090],[892,1988],[1158,682]],[[353150,789760],[-860,-2725]],[[352290,787035],[147,-1173],[945,1863],[357,-1304]],[[353739,786421],[-1155,-5143],[-1519,-7]],[[351065,781271],[-55,2711]],[[351010,783982],[-1493,-1524]],[[349517,782458],[846,3001]],[[350363,785459],[-896,2801]],[[349467,788260],[-1030,-2871]],[[348437,785389],[-817,58]],[[347620,785447],[-1275,-2839]],[[346345,782608],[-1470,-189],[18,1172],[964,527]],[[345857,784118],[1727,2431],[-1380,533],[-190,-946],[-1187,171]],[[344827,786307],[12,1712]],[[344839,788019],[-1010,-875]],[[343829,787144],[-2031,-574]],[[341798,786570],[-3835,606]],[[337963,787176],[-2177,-629]],[[335786,786547],[-431,2517]],[[335355,789064],[2616,3120]],[[337971,792184],[-1073,450],[869,2880]],[[337767,795514],[1177,861]],[[338944,796375],[-99,1854],[1994,6850],[1521,3414]],[[342360,808493],[2013,1738],[1575,-189]],[[341569,796584],[-2717,-3564],[704,54],[2013,3510]],[[896558,826971],[596,-1498],[-161,-2055],[849,-2951],[278,-4044],[-467,-3138],[834,-3629],[1001,-7042],[1267,-5754],[969,-2942],[-1376,2332],[-1092,614],[-1743,-671],[-1474,-6675],[-49,-1980],[1246,-3053],[590,-2534],[744,-254],[264,-2318],[-414,-1968],[-414,3143],[-1957,840],[-1391,-4644],[-686,3163],[579,4083],[-207,2651],[603,2523],[-875,4365],[766,4852],[-197,5604],[377,4192],[-1346,3044],[-171,3179],[396,1674],[56,4643],[1952,641],[500,2656],[-1032,2280],[1185,671]],[[980032,818789],[1734,-952],[-1392,-593],[-1224,1101],[882,444]],[[487744,825735],[-1037,-665],[769,1798],[268,-1133]],[[275745,817216],[-3632,1793]],[[272113,819009],[533,807]],[[272646,819816],[1977,116]],[[274623,819932],[1122,-2716]],[[488342,820617],[-491,-1109],[-540,1495],[1031,-386]],[[538081,826905],[-958,-811],[-522,1768],[684,919],[796,-1876]],[[482727,825162],[530,-6881],[-827,-4031],[-3340,-876],[-381,-705],[-3192,-2340],[-2838,-601],[710,1220],[-1503,-526],[1450,1622],[-1348,-612],[-819,579],[1685,2259],[-401,1894],[1116,1253],[599,1874],[-2271,2671],[724,3240],[-556,963],[4197,-98],[1145,2367],[-1752,239],[1359,2756],[2065,281],[868,-603]],[[479947,831107],[3027,743],[1519,-3282],[-68,-2316],[-1698,-1090]],[[491362,851389],[-286,-1151],[-2160,-2146],[-401,-2258],[2032,772],[3691,-35],[823,-1236],[-2263,-5523],[-1728,-1051],[1561,-389],[-1972,-1722],[2120,-2],[1256,-736],[1366,-1971],[1010,-4719],[3354,-3886],[-337,-570],[1560,-5105],[-861,-1507],[2805,316],[1670,-1216],[249,-1687],[-1311,-3695],[-1451,-685],[-183,-2033],[2024,-139],[-48,-1073],[-1215,-1517],[-2098,-966],[-2751,15],[-1754,779],[-1720,-1740],[-2676,672],[-1126,-500],[-1080,-2388],[-1053,958],[-1543,-594],[-1381,-1595],[-325,1332],[2109,3140],[1097,2442],[2921,99],[1953,3174],[-2388,-2076],[-3632,2057],[-838,-660],[-1000,1505],[2442,1879],[1119,2040],[-336,2214],[-1616,-648],[1589,2446],[2625,1041],[669,2003],[160,2634],[-829,-293],[-1120,2013],[375,2939],[-1455,-1083],[-3271,454],[1275,3814],[-598,1172],[132,2086],[-1505,-1666],[-1061,-2414],[440,4103],[1170,4164],[-1289,-1339],[-1334,1102],[1585,3049],[-128,3843],[1667,2259],[-162,1526],[5593,679],[-157,-707]],[[589039,490923],[-572,-747],[-84,1557],[656,-810]],[[592152,491925],[-899,584],[683,564],[216,-1148]],[[258356,772744],[-801,-2405]],[[257555,770339],[-323,707]],[[257232,771046],[1124,1698]],[[268828,776662],[1113,441]],[[269941,777103],[234,-736],[2217,752]],[[272392,777119],[160,-2629]],[[272552,774490],[-3724,2172]],[[256275,785735],[-1336,-1321]],[[254939,784414],[-604,-1184]],[[254335,783230],[-490,967]],[[253845,784197],[615,1271]],[[254460,785468],[1815,267]],[[516113,815548],[-838,-971],[-918,455],[1358,1238],[398,-722]],[[189596,872516],[-2362,-2950],[-1044,1470],[3406,1480]],[[581880,871836],[-1354,-975],[-795,1658],[1719,282],[430,-965]],[[309615,809192],[-402,-1402],[-1162,1505],[715,1124],[849,-1227]],[[708031,725294],[-1442,-438],[470,-803]],[[707059,724053],[-1499,-1179],[-646,387],[-3185,-349],[-2784,-2329],[-1209,-2336],[673,-1235],[536,-3855],[-1819,-3866],[239,-2848],[-1766,-588],[-1170,600],[-352,-913],[1156,-3132],[-1011,-1519],[-1163,-548],[-722,-3475],[105,-2943],[-1140,-1792],[-754,999],[-1212,0],[-1619,-1756],[443,-963],[-1252,-747],[-1008,520],[-1464,-2331],[-612,-6378],[-3004,-1636],[-1595,30],[-1174,-1023],[-1475,629],[-3031,-532],[-4536,2668]],[[669009,681613],[724,1598]],[[669733,683211],[1889,4168],[-344,3262],[-2332,668],[24,4468],[-747,5264],[990,2176],[-1128,792],[-70,2701],[1122,1331],[-454,1178],[625,803],[864,5722]],[[670172,715744],[779,-959],[1226,-83],[900,-1617],[2080,1629],[144,2209],[2094,1148],[1802,1945],[848,4688],[2051,706],[584,1884],[2103,-1308]],[[684783,725986],[1519,-81],[1917,-963]],[[688219,724942],[857,-1318],[2480,2224],[846,-1284],[631,2634],[2109,659],[-102,1541],[1845,3152],[1047,-885],[63,-2302],[760,87],[-331,-4773],[457,-2338],[568,-228],[3915,4231],[1415,61],[80,-1108],[1417,1088],[1110,-124],[645,-965]],[[566573,440308],[223,-3162],[-210,-1365],[56,-4659],[-303,-2233],[224,-1122],[-5511,-74],[2,-17504],[475,-3801],[429,-547],[2988,-5635]],[[564946,400206],[-5455,-2133],[-1865,-113],[-979,784],[-3657,413],[-996,678],[-893,1800],[-7308,58],[-5077,5],[-1484,2257],[-840,238],[-1537,-1453],[-1483,262],[-753,-478]],[[536300,469971],[3696,-165],[5324,160],[1118,-2226],[-24,-1364],[765,-4655],[1532,-4849],[3103,828],[1910,-181],[519,4871],[369,636],[2606,604],[25,-2030],[3176,-164],[252,-684],[-171,-2633],[321,-2818],[-184,-4902],[75,-2523],[948,-2644],[303,-3855],[-358,-1191],[280,-1789],[784,819],[1655,-111],[676,583],[1205,-221],[368,841]],[[533384,475069],[1017,2282],[1379,1031],[533,-1124]],[[536313,477258],[-1726,-2587],[145,-3699],[-806,-372]],[[555733,756786],[1170,-1918],[225,-2072]],[[557128,752796],[-215,-3561],[700,-2177],[620,-328]],[[558233,746730],[186,-1133],[-1038,-3206],[-962,-818],[-289,-1931],[-571,332]],[[553728,752769],[-3,1422]],[[553725,754191],[148,-125]],[[553787,755260],[-14,-11]],[[553773,755249],[822,2019],[1138,-482]],[[656633,652705],[-901,-1424],[-745,766],[-96,-3705],[623,-1063],[-1215,-426],[-109,-1581],[-858,-4087],[-39,-1959]],[[653293,639226],[-226,-489],[-7081,1844],[-2674,6790],[-67,1228]],[[655778,659124],[179,-2205],[425,-236]],[[309296,179739],[65,13040]],[[325969,372995],[1214,-2244],[794,-2648],[3023,-4732],[2632,-1395],[1443,-2135],[2799,-2994],[1510,-1049],[718,-1999],[-1777,-5378],[32,-1472],[-1251,-3354],[102,-701],[1213,243],[4809,-1661],[758,1376],[1249,-553],[416,1569],[2054,2949],[410,2035],[172,4341]],[[348289,353193],[1281,314],[732,-864],[611,-3295],[-464,-5309],[-1358,-1791],[-1524,-1041],[-627,-1585],[-1733,-1999],[-1389,-3158],[-1981,-5081],[-1862,-3513]],[[339975,325871],[-732,-2389],[172,-1585],[-992,-6008],[-93,-3549]],[[309879,194532],[-49,393],[-4164,1672],[-5440,110],[-1359,2659],[188,5089],[-2258,-334],[-967,3631],[-209,3213],[319,1594],[906,79],[427,1919],[1020,1089],[17,1621],[876,1719],[-625,2090],[478,2273],[1225,1724],[-98,2195],[680,1497],[-501,2476],[678,1225],[-318,2221],[1090,2064],[-1676,2601],[1933,168],[135,1907],[-1687,344],[388,2687],[-624,2900],[343,1619],[-1014,1048],[61,4098],[1010,1166],[-418,2671],[-58,5681],[658,2112],[-342,939],[775,3402],[316,3654],[1317,1465],[-213,4131],[-387,1652],[137,3839],[-205,1604],[379,1895],[1808,2737],[-182,4358],[501,3515],[661,2560],[554,453],[-116,2920],[207,2652],[-556,73],[-416,4738],[-1154,5345],[182,2495],[995,4195],[570,486],[79,3490],[-275,2637],[552,1308],[475,4086],[1341,2896],[911,4568],[1390,745],[-654,3019],[463,2161],[-516,3958],[600,2332],[-493,1506],[866,2641],[2483,2122],[965,6117],[-517,1064]],[[313347,369511],[1342,3587],[963,607],[403,1844],[1247,-1760],[1982,-19],[1256,-746],[778,-3548],[970,4473],[438,398],[2709,48],[534,-1400]],[[629140,735218],[-1045,-171]],[[628095,735047],[-1011,4059],[-1605,45],[-392,1153],[-731,-365]],[[624356,739939],[-1331,1995],[-1609,748],[-391,1871],[426,1405],[-786,2296]],[[620665,748254],[4338,1089]],[[625003,749343],[1628,-2630],[-587,-1238],[1635,-2395],[-1069,-1518],[1728,-2269],[490,-1257],[312,-2818]],[[547091,792639],[-243,-1257],[783,-2256]],[[547631,789126],[-224,-1768],[-1433,236],[-271,-4387],[-1001,-852]],[[544702,782355],[-376,-1098],[-2658,-307],[-1381,-1238],[-2232,611]],[[538055,780323],[-3644,1082],[-608,2248],[-2569,-631],[-609,-1058],[-1590,402]],[[529035,782366],[-2424,1140]],[[526611,783506],[-146,1264]],[[526465,784770],[74,1425]],[[527029,786288],[-98,96]],[[526931,786384],[1715,-1361],[327,1349],[1529,-847],[3412,1897],[1324,-290],[912,-1134],[-555,4045],[2391,2146],[388,1445]],[[538374,793634],[651,-975],[1784,-19],[780,2279],[3014,-1357],[1168,269],[1320,-1192]],[[628095,735047],[-1763,761],[-1840,3816]],[[624492,739624],[-136,315]],[[635746,732426],[-767,-144],[-1582,2417],[608,947],[-294,1975],[517,514],[-907,1688],[-619,-210],[-3562,-4395]],[[625003,749343],[777,940],[1422,-1334],[1847,-913],[596,1283],[-1362,2193],[688,1386]],[[628971,752898],[888,-464],[1421,-2948],[1667,-606],[1977,3743]],[[584871,490497],[-360,-1430],[252,-1635],[737,-400],[28,-1715],[-1015,-1862],[-771,-2941],[-1390,-2187]],[[582352,478327],[16,203]],[[581384,484839],[-244,85]],[[581140,484924],[38,1702],[-583,1975]],[[580595,488601],[135,697],[909,-1220],[1328,546],[172,2233],[1732,-360]],[[515815,805530],[552,-132]],[[516367,805398],[282,-12]],[[516649,805386],[1030,-2573],[-689,-1157]],[[516990,801656],[-1035,-1192],[126,-2260]],[[516081,798204],[-784,-162],[-1776,1643],[-21,2060],[-1901,-1041],[-3,1696],[-1410,464],[-1556,2693],[-742,-400],[-875,2283]],[[509305,809102],[1534,-1008],[900,1060]],[[511739,809154],[770,523],[2704,-1124],[973,-945],[-371,-2078]],[[509987,574011],[-300,-1782],[636,-1871],[328,-2798],[-718,-2009],[-52,-2138],[-645,-764],[-778,-4115],[-751,-209],[-246,-6960],[246,-6885],[-191,-2029]],[[504506,541548],[432,461],[-556,2327],[131,1836],[-69,12162],[-488,1392],[-262,4218],[-1528,2148],[335,3754]],[[502501,569846],[1462,2689],[1538,-170],[1135,2836]],[[506636,575201],[-65,1924],[1423,864],[1993,-3978]],[[500603,593059],[-148,-2454],[1262,-4703],[1619,-2049],[-591,43],[-3,-1913],[1605,-2408],[1413,465],[423,-1468],[-374,-1115],[827,-2256]],[[502501,569846],[-1157,-7],[-1535,732]],[[499809,570571],[-641,304],[-1117,-1054],[-5912,56],[-236,-2406],[356,-1128],[57,-4407],[195,-1047]],[[492511,560889],[-336,-329],[-1131,2782],[-1816,-3],[-1262,-1476],[-1772,1684],[-361,1846],[-1177,1093]],[[484656,566486],[92,3651],[530,969],[32,3685],[1362,1210],[1026,1810],[-145,1982],[705,720],[-284,1927],[772,1561],[1320,-1116],[762,513],[287,2323],[781,40],[120,1607],[1158,1915],[955,-626],[389,1707],[571,175],[1995,1976],[803,1352],[1457,69],[1259,-877]],[[757152,634925],[157,-3980],[-836,791],[-419,-869],[401,-2970]],[[747364,635607],[-862,7959],[-482,1409],[461,3297],[-1633,1510],[-339,842],[351,1699],[454,-195],[397,1817],[1205,35],[-336,1754],[-880,497],[-1021,1860],[1204,3729],[457,-1339],[2409,-1697],[192,1247],[566,-1625],[-24,-3768],[1737,-875],[4473,69],[1163,-1335],[-603,-290],[-521,-3085],[-519,-1061],[-1416,-603],[-574,-2565],[430,-3295],[845,-739],[884,3110],[-23,1075],[879,-15],[321,-4470],[361,-1443],[232,-4191]],[[577817,753361],[-1332,-286],[-666,940],[-1888,-679],[-818,-1471]],[[573113,751865],[-708,-257],[248,-1412],[-2511,-1133],[-2036,1830],[-2453,-982],[-1998,-299]],[[563655,749612],[249,2255],[-469,1639],[-1369,1898]],[[562066,755404],[499,753],[-158,2377],[1417,2048],[-1173,1579],[-372,3276],[790,1365]],[[563069,766802],[899,-947],[-305,-1443],[849,234],[6313,-1203],[1996,1993],[2420,949],[2215,-1068],[460,-976],[1487,-475]],[[552797,770541],[949,71],[-547,-3427],[1200,-1535],[-941,-464],[675,-1549],[-816,-1009]],[[553317,762628],[-466,-1427],[-979,-366],[-640,-1554],[-21,-2421]],[[551211,756860],[-2135,1999]],[[548847,759103],[-865,3006],[-1556,1974],[-1387,2584],[-233,1533],[-1094,1730],[143,2448],[1404,-1008],[659,1230],[3561,-820],[2361,-4],[957,-1235]],[[578188,837332],[1797,-1187],[1612,-22],[297,-1505],[2088,951],[1870,-1630],[197,-3078],[-497,-1583],[895,-799],[785,-2681],[1079,-829],[-105,-1454],[1933,-697],[706,-2112],[-1562,-1453],[-2012,622],[-442,-1063],[768,-1294],[117,-2880],[517,-1252]],[[588231,813386],[-2174,-324],[-1244,-2665],[32,-1963],[-1066,1260],[-2262,-563],[-585,1389],[-951,-633],[-1693,578],[-2538,33],[-356,822],[-3380,955],[-4343,-272],[-2101,-2070]],[[565570,809933],[131,3095],[-1266,1283],[1800,2413],[118,2152],[-1118,5405]],[[565235,824281],[3565,206],[2164,2116],[867,3481],[2545,2096],[-883,411],[377,1926]],[[573870,834517],[1275,965],[1457,-188],[1586,2038]],[[253072,598860],[-954,23],[211,11377]],[[252329,610260],[78,924],[908,-31],[788,2846],[631,157]],[[338445,385253],[-57,2053],[-2529,3151],[-2546,-67],[-4860,-2061],[-445,-2429],[-998,-3004],[-1,-2984],[-1040,-6917]],[[313347,369511],[-1901,-7],[-303,4537],[-550,2598],[-29,1886],[-936,2231],[94,1845],[-681,910],[130,4369],[654,1708],[-1404,2753],[-349,5437],[-609,636],[-549,2589]],[[306914,401003],[-317,1812],[518,663],[1114,3294]],[[308229,406772],[86,-108]],[[307332,412821],[-12,12]],[[307320,412833],[534,1615],[-562,1622],[388,2167],[985,2360],[-538,3056],[252,1105],[13,3652],[815,2240],[-2481,9184]],[[306726,439834],[2028,-352],[336,-660],[915,614],[907,1871],[972,119],[387,1049],[1308,1404],[1060,1739],[1295,885],[2410,673],[230,-3202],[-352,-1975],[323,-2598],[5,-2456],[915,-3174],[1331,-1634],[258,-1119],[2319,-469],[664,-955],[775,65],[839,-1944],[1637,-809],[1073,-2321],[1980,213],[1585,-1778],[89,-2340],[488,-2570],[71,-2786],[-861,-56],[947,-2259],[185,-4679],[4549,-350],[535,261],[-348,-2168],[208,-3460],[1565,-1647],[718,-4544],[-629,-4749],[-920,-3931],[752,-1393],[-830,-1096]],[[343103,516223],[1286,-592],[290,1169],[-594,1540],[537,1287],[571,-655],[2088,1135],[1007,-1605]],[[348288,518502],[1350,-1219],[1007,1385],[2230,-1015],[734,1068],[1972,7928],[939,2129]],[[351748,304813],[-452,1152]],[[351296,305965],[18,-30]],[[352392,311289],[-83,-134]],[[352309,311155],[-1203,1592],[-444,2051],[-1275,1195],[-1020,2192],[-1852,1538],[-841,2071],[-1243,-1204],[-476,2670],[-1824,3088],[-1060,-1043],[-1096,566]],[[348289,353193],[15,849]],[[348304,354042],[381,1259],[573,6839]],[[349258,362140],[-996,1501],[-992,-960],[-1066,-98],[-477,2430],[-221,5388],[-643,2156],[-946,156],[-570,1117],[-1506,-1058],[-2830,960],[349,6583],[-915,4938]],[[306726,439834],[-1061,129],[-1199,-762],[-695,286],[15,9077],[-1669,-2890],[-2622,-223],[-548,2924],[-2307,585],[654,2478],[-1597,3834],[-629,2426],[153,912],[-783,1342],[783,1462],[-105,2390],[1438,2025],[292,1301],[-278,1457],[710,2747],[258,3034],[523,329],[2372,3334],[2420,912],[483,1049],[1097,138],[460,-895],[759,385]],[[305650,479620],[1571,18018],[-742,4221],[-1120,2035],[47,4251],[1616,897],[827,-561],[31,1355],[-551,1185],[-1363,-27],[10,3846],[4644,65],[-48,1584],[567,-1389],[1362,2105],[410,-131],[727,-2787],[-10,-2401],[605,77]],[[314233,511963],[1595,-2791],[1723,1371],[578,-1731],[1027,2469],[1034,861],[1713,2168],[220,1690],[1782,1884],[13,1122],[-1832,671],[31,1638],[-503,2388],[-6,2267],[-1658,3821],[1562,-545],[650,-1251],[2019,-41],[906,-1945],[567,468],[145,2044],[838,822],[715,-345],[1664,1122],[761,1357],[770,109],[1107,2721],[-382,1229]],[[331272,535536],[1666,218],[421,-924],[-439,-3256],[1237,-901],[423,-2652],[-843,-2050],[65,-1412],[-453,-3906],[664,-2463],[-3,-2213],[1459,-3108],[1024,-1021],[974,479],[475,1795],[2073,691],[1321,1836],[784,-787],[983,361]],[[819833,533745],[518,-3074],[-610,57],[-223,3017]],[[819518,533745],[-778,-1076],[260,-1925],[-644,-2187],[-1513,3369]],[[816843,531926],[317,-10]],[[754532,669180],[-103,-1199],[1100,-637],[230,-3171],[-1116,-669],[-2589,-179],[-1094,703],[-1617,-1119],[-2517,1540],[94,2101]],[[746920,666550],[1793,4688],[1234,1207],[1020,-398],[12,-970],[2014,-627],[411,574],[1055,-708],[73,-1136]],[[570163,399300],[-97,-721],[1492,-4348],[1131,-5268],[1417,-2100],[1509,-1499],[164,-1972],[1164,-308],[-84,-3161],[1045,-3014],[2755,-1412],[193,-1507],[716,-760]],[[581568,373230],[-652,-114],[-806,-1586],[-1749,-1260],[-888,-2253],[-2510,-3737],[-422,-3177],[-1064,-2025],[-1499,-976],[-912,-5088],[-390,-641],[-1932,-610],[-2373,1283],[-1744,1980],[-1075,-1133],[-663,-3633],[-1526,-3016],[-1235,-1623],[-2518,31],[-314,2400],[523,1652],[-61,1477],[-1244,5248],[-1013,1499]],[[555501,357928],[-9,16450],[2760,0],[9,21810],[2881,712],[3024,1120],[552,-106],[783,-2521],[2162,2813],[477,-441],[1051,1370],[972,165]],[[563500,569410],[1256,-3150],[928,-3348],[-66,-2857],[-429,-1338],[192,-1771],[1694,-890]],[[567075,556056],[401,-2217],[1560,-911],[1095,-2447],[-159,-1216],[1941,-2692],[1314,-2545],[-148,-1067],[571,-2287],[1581,-1732],[889,-3956]],[[576120,534986],[-801,526],[-814,-803],[-3603,1479],[-766,-1703],[-1343,-560],[-1239,380],[-2507,-1961],[-837,437],[-1000,-535],[-927,-3032],[-2042,868],[-415,-217],[-2721,1291],[-921,2174],[-1166,1538],[-849,227],[-1201,-1399],[-1392,-3755],[119,-4616]],[[551695,525325],[-378,856],[-871,-729],[-2008,1094],[-2124,-885],[-492,-1933],[-77,-2234],[-792,-3328]],[[544953,518166],[-333,3783],[-801,1295],[-1795,4145],[-295,3150],[-871,1819],[-406,3640],[150,3467],[-516,1028],[856,1428],[1407,5829],[651,1541]],[[543000,549291],[1013,-287],[1483,1234],[463,1078],[665,-1864],[2402,2563],[2617,458],[1436,3527],[-612,1384],[714,748],[1453,29],[1871,629],[1198,1650],[1363,3371],[1283,2322],[-54,1234],[935,1469],[1252,1028],[1018,-454]],[[313542,772321],[-966,631]],[[312576,772952],[63,1967]],[[312639,774919],[-31,200]],[[311702,775814],[-38,-22]],[[311664,775792],[-16,7865],[-1191,1559],[-1648,-845]],[[308809,784371],[-1151,1538]],[[307658,785909],[-2124,-4467]],[[305534,781442],[-802,-4756],[-1671,-3814]],[[303061,772872],[-1193,164]],[[301868,773036],[-528,-1674]],[[301340,771362],[-8865,-22]],[[267330,873831],[-420,723]],[[292475,771340],[-1307,-619]],[[291168,770721],[-1718,-2421]],[[289450,768300],[-20,24]],[[279963,760904],[422,241]],[[280385,761145],[-13,-1049]],[[270430,756850],[633,2722]],[[251257,789167],[-84,-67]],[[251173,789100],[-3508,1179]],[[247665,790279],[-1884,-843]],[[245781,789436],[-4105,3279]],[[241676,792715],[-1975,-511]],[[239701,792204],[-2537,1286]],[[237164,793490],[-255,716]],[[236909,794206],[-395,2614],[-834,385],[-19,-2240],[-6577,10],[-10660,-1]],[[218424,794974],[-4738,0],[-7106,0],[-10660,0],[-8290,0],[-5922,0],[-5922,0],[-8292,0]],[[167494,794974],[-8574,0]],[[138819,835824],[-202,1310]],[[138617,837134],[-4105,2900]],[[134512,840034],[-691,-52]],[[133821,839982],[-576,2586],[-4969,9944],[-3121,3456]],[[125155,855968],[-298,1719]],[[124857,857687],[-1180,1271],[-2349,-1115]],[[121328,857843],[-714,-2681],[-2388,-1476]],[[118226,853686],[-430,1914]],[[117796,855600],[-4423,5079],[295,1541],[-2484,-951]],[[111184,861269],[-2857,694]],[[108327,861963],[0,55397]],[[526465,784770],[146,-1264]],[[529035,782366],[-61,-1865],[-955,295],[-410,-1411],[-1912,-444],[-826,-2706],[-1620,3644],[-1618,-3101],[-2130,24]],[[519503,776802],[-732,2903],[-913,87],[-951,-1680],[-73,1667],[2612,5298],[146,989],[1562,611]],[[521154,786677],[3515,378]],[[524669,787055],[685,84]],[[525354,787139],[153,0]],[[304393,396029],[1367,827],[206,2976],[948,1171]],[[868915,771709],[-4,489]],[[866863,773017],[-131,-309]],[[866732,772708],[-479,546],[-1125,-2031],[-1011,-439],[480,-4967],[17,-3784],[-536,-3144],[-1788,-1038],[284,-1135]],[[862574,756716],[-796,2111],[-950,631],[-496,-3100],[-1128,-364],[-1084,-2223],[-2440,-301],[683,-2516],[-499,-1028],[-2588,842],[-767,1479],[-841,-830],[-307,-1676],[-1392,-2686],[-3055,-2636],[-1465,-2700]],[[817405,638260],[-696,-172]],[[815489,636816],[-446,-704]],[[799923,632140],[-474,813],[-1252,-215],[-958,1686],[-952,506],[-354,2468],[678,2272],[-662,767],[-1942,85],[-1576,2503],[-1141,-1238],[-192,-1334],[-1177,-1227],[-883,286],[-386,-1268],[-819,1444],[-414,-1094],[-475,990],[-819,-1845],[-1356,1706],[-1082,-2143]],[[783687,637302],[-1267,492],[-408,-1236],[589,-2531],[-88,-4007],[-1335,436],[-237,2037]],[[780941,632493],[-51,1058],[-1637,-1706],[-879,29],[-657,1413],[-169,1934],[-2013,580],[534,4142],[-123,1605],[-1325,565],[-88,2566],[-420,1288],[425,1474],[-2270,-149],[-1256,-915],[350,1302],[-442,2138],[800,4503],[981,2031],[1258,1375],[295,4483],[-224,5860],[-1139,537],[-395,2839],[-1558,2179],[-599,-1731]],[[770339,671893],[-2000,1433],[-891,-283],[831,2083],[-404,1700],[-759,-835],[494,1778],[-846,1406],[-1443,-1426],[-266,-901],[-1807,720],[-407,809],[-2002,-3017],[-1933,-1258],[-242,-1117],[-1160,-1512],[-104,-1174],[-1907,-1295],[-961,176]],[[746920,666550],[-396,1219],[277,2055],[-632,1322],[-1420,-1311]],[[744749,669835],[-2690,-191],[-1631,1463],[-406,-928],[-915,918],[-347,-920],[-765,2068],[-1544,229],[101,1636],[-1235,20],[-1349,1873],[-354,1826],[-1438,-215],[-1189,2542],[-837,419],[-1932,2558],[-321,1254],[-1739,64],[-450,-1448],[-680,422]],[[725028,683425],[-912,1483],[-1363,910],[-741,1898]],[[722012,687716],[-22,32]],[[721990,687748],[-1520,1101]],[[720470,688849],[-85,152]],[[720385,689001],[-645,1760],[-875,-646],[-201,3519]],[[718664,693634],[-916,3746],[865,457],[606,-1415],[834,846]],[[720053,697268],[-8,373]],[[720045,697641],[-226,3602]],[[719819,701243],[-63,322]],[[719756,701565],[-863,1620],[-136,3483],[605,833],[-833,1717],[-1080,805],[-749,3537],[-560,1383]],[[716140,714943],[-31,68]],[[716109,715011],[-981,-120],[-1888,1102]],[[713240,715993],[-598,1335],[-1121,-344],[-675,1538],[193,1741],[-372,1584],[-1156,524],[-215,1038],[-2237,644]],[[708031,725294],[631,913],[-623,1278],[-415,5383],[-1298,887],[-1322,-313],[-17,2341],[-455,2647]],[[704532,738430],[786,934],[214,2587],[2361,1788],[66,880],[1994,662],[261,-1774],[2230,851],[954,3157],[3611,553],[664,1753],[2586,2436],[2562,1479],[-18,934]],[[722803,754670],[-124,2817],[1040,1232],[-414,1005],[1099,702],[-1196,5543],[279,3844],[-1273,303],[172,1240],[2205,727],[2330,1304],[826,-1111],[1360,-226],[288,1889],[-711,459],[1953,9870],[2740,-1276],[1807,11],[332,-841],[1941,1380],[477,1133],[-363,3916],[621,2780],[2222,852],[566,2845],[1582,456]],[[742562,795524],[1366,453]],[[743928,795977],[-198,-1664],[657,-1934],[1493,-1010],[1474,-2263],[1426,8],[2089,-1942],[509,-2316],[1038,-1959],[455,-2521],[-89,-2922],[-945,-3025],[599,-1951],[1963,-707],[3343,-242],[2414,-798],[2932,-3260],[1773,-431],[1561,-6349],[1315,-2879],[2278,411],[6283,-1313],[1434,647],[4806,-1253],[719,-1481],[3056,-1244],[1773,-1508],[2185,744],[0,-1293],[1345,-374],[597,844],[4370,3263],[3892,939],[3533,51],[2659,1883],[1686,3363],[2571,2192],[-1475,3886],[609,2724],[768,1404],[1427,-35],[515,-833],[2750,-1019],[1232,1167],[1352,2500],[3233,555],[1555,2001],[894,2926],[2140,427],[2709,2104],[1488,256],[2396,-915],[532,1493],[-519,1731],[-1748,2986],[-1621,1955],[-2027,23],[-1161,-1989],[-1639,1288],[-1471,-68],[-924,-1014],[-946,1529],[1100,4410],[2026,6721]],[[824119,799896],[3306,-1839],[1606,1961],[2246,1315],[-268,2012],[2508,7077],[1708,2206],[-70,3518],[-1635,391],[75,915],[1693,2279],[4538,1855],[3898,153],[2976,-2233],[730,413],[1594,-956],[1844,-3806],[1368,-5297],[332,-2403],[1061,-2324],[-2,-1506],[789,-1449],[-243,-1989],[1381,-1805],[1956,186],[1156,-1410],[1050,159],[1939,-2946],[992,-180],[697,-3080],[-256,-1266],[808,-2584],[2173,-65],[2158,521],[402,1058],[2116,889],[2291,1637],[751,-306],[523,-3592],[-1623,-2448],[96,-1032],[-932,-3727],[-15,-1488],[-1876,-4461],[-201,-2157],[-844,-383]],[[492511,560889],[-141,-2202],[406,-1832],[263,-3506],[-789,-1640],[-545,-4307],[-694,-2356],[98,-2719],[662,-4178],[468,-254],[-61,-2649],[-566,-132]],[[479041,530496],[-66,4321],[385,1445],[-67,3062],[-952,792],[-254,1539],[-1986,1617],[752,1741],[100,1614],[-527,2870]],[[476426,549497],[707,-10],[598,3484],[-665,645],[53,1196],[1544,-268],[-750,2230],[480,1742],[-327,1985],[-670,473],[2,3118],[405,832]],[[477803,564924],[915,1570],[1926,-1488],[763,1026],[109,1819],[685,-498],[406,898],[55,-2635],[575,-500],[530,1153],[889,217]],[[544953,518166],[-344,-3518],[-883,1414],[-2331,576],[-1162,845],[-3307,40]],[[536926,517523],[-203,562],[-5200,257],[-55,-784]],[[531468,517558],[-3747,1],[-497,811]],[[523766,532889],[681,2620],[720,4809],[1666,3097],[333,1352],[1010,1400],[941,-623],[344,1018],[1184,-2164],[336,-1540],[1106,1537],[79,1135],[782,1348],[-261,923],[690,1881],[604,4103],[473,1856],[1119,1724],[342,3198],[683,671],[262,2942],[738,3370],[991,3170],[1854,2087],[187,3651],[-300,1123],[-893,507],[-371,4116]],[[539066,582200],[1256,-585]],[[540322,581615],[681,-1920],[889,-4800],[-143,-4336],[684,-4480],[1052,-2071],[-2275,-392],[-1646,226],[-739,-1708],[986,-2891],[2178,-3828],[908,-4180],[103,-1944]],[[576120,534986],[1069,-2752],[1122,-1744],[654,-155],[832,1072],[1179,-692],[883,1325],[576,-148],[1439,-3584],[871,-867],[917,-2043]],[[585662,525398],[-235,-2660],[258,-1154],[-328,-2320],[1484,-1752]],[[586841,517512],[-11,-38]],[[584660,512056],[-1486,-2485],[-23,-1897],[-698,-3669]],[[582453,504005],[-110,3]],[[582340,501712],[2,5]],[[582342,501717],[-184,-5222]],[[582158,496495],[-988,-1767]],[[581170,494728],[-66,250]],[[580139,490215],[21,11]],[[580160,490226],[435,-1625]],[[581140,484924],[-41,14]],[[584945,456249],[-9,-232]],[[584936,456017],[-4664,-1572],[55,-1274],[-1437,-3106],[637,-3593],[25,-4964],[-783,-4821],[349,-1950],[1616,-3180],[1009,-488],[367,1355],[654,279],[0,-7331],[-670,852],[-1499,-710],[-1824,5254],[-2290,1699],[-936,3496],[-686,-1740],[-981,-434],[-1584,486],[-1880,1582],[-83,2288],[-2734,-798],[-42,1776],[-982,1185]],[[536313,477258],[950,-1200],[751,881],[88,1388],[622,-179],[1160,1097],[145,-3151],[826,-299],[2478,5041],[757,573],[762,2785],[196,2570],[-6,5051],[904,2000],[1205,4149],[845,831],[1317,2669],[-80,1609],[454,3031],[41,5237],[432,2469],[40,2835],[1163,5398],[332,3282]],[[530917,481515],[1039,2346],[958,-1045],[236,2240],[-1101,2855],[188,2927],[1275,-414],[1061,489],[-40,2376],[1004,-17],[551,-2260],[1314,-486],[747,1522],[425,-1938],[557,-8],[824,3418],[199,2825],[-125,2613],[194,2096],[-1723,2459],[68,2335],[563,2047],[964,1630],[-704,3310],[-916,287],[-1603,-1053],[-309,2412],[363,3042]],[[301889,574992],[-1773,-1158],[-807,-2784],[-548,-487],[-1176,-3691],[-381,-4160],[-972,-3331],[828,194],[728,-892],[363,-2852],[692,-1455],[-74,-5493],[654,-501],[875,-2251],[2443,24],[995,524],[1555,-858],[1822,-4758],[2687,128],[2511,505],[357,-1281],[-1071,-4473],[-84,-4524],[538,-3807],[973,-2657],[-1454,-3099],[600,-588],[1133,-2390],[930,-6914]],[[305650,479620],[-1038,2498],[-1099,196],[1837,6109],[-61,546],[-2274,2604],[-1340,-684],[-988,1074],[-644,-1030],[-1142,-606],[-1366,121],[-742,772],[-118,2654],[-832,813],[-466,2631],[-1617,1649],[-477,2310],[-1066,2255],[-1341,553]],[[290876,504085],[-1653,1527],[-1198,1762],[-510,-1262],[-1182,196],[-1396,926],[-125,1254],[-2346,2427],[-1521,2424]],[[283608,547547],[469,2853],[404,-994],[1085,2544],[-784,3116],[289,947]],[[270656,561454],[-1045,-756],[-1,-2305],[553,-642],[-488,-1252],[154,-1699],[-450,-815],[400,-1454]],[[261820,570254],[343,725],[1978,-1417],[578,633],[980,-428],[500,-1182],[992,-220],[470,1031]],[[594456,712459],[-1330,-157],[-394,735],[-1864,49]],[[541137,806029],[512,920],[1002,-1200],[2959,-1412],[-582,-888],[1302,-1932],[863,826],[-304,1127],[2763,-2695],[1910,-550],[749,-2184]],[[552311,798041],[-1865,-1501],[-1117,-2187],[-1584,-162],[-654,-1552]],[[538374,793634],[-3286,4113],[-987,3444],[489,1821],[5323,3251],[1224,-234]],[[527054,829528],[17,-108]],[[539583,823049],[24,-14]],[[539607,823035],[433,-2642],[-766,-2078],[1335,-2395],[370,-2647],[-419,-1477],[1152,-3435],[-575,-2332]],[[526931,786384],[-142,138]],[[521154,786677],[-87,2795],[705,3387],[823,2000],[-1899,1057],[-1987,51],[-1086,1730]],[[517623,797697],[397,2049],[-1030,1910]],[[516649,805386],[-276,1385],[830,2990],[-680,1620],[2204,880],[815,2780],[450,5344]],[[524116,829329],[-32,661]],[[524084,829990],[2970,-462]],[[620127,572847],[-898,-2965]],[[619229,569882],[-1014,483],[-2109,-595],[-89,3606],[1700,5198]],[[617717,578574],[810,-533],[1241,1968]],[[526959,830136],[95,-608]],[[524084,829990],[-25,488]],[[300643,611589],[18,1790],[-662,1520],[714,800],[239,2357],[-339,3480]],[[523823,723550],[-1021,-2619],[388,-754],[-286,-2947],[412,-3949],[-412,-2784],[-2033,-3872],[-38,-1469],[642,-3341],[1333,-2025],[340,-2270],[1974,-2792],[1318,-10918]],[[526440,683810],[-579,-677],[917,-2836],[562,-3966],[-75,-2410],[279,-4589],[-468,-2695],[408,-2861],[-97,-1753],[-1023,-1293],[-119,-1579],[1534,-4355],[76,-1665],[633,-2726],[1195,-235],[2363,-1543],[1198,-4579]],[[533244,644048],[-5754,-7235],[-6708,-8434],[-4570,-8259],[-4469,-1992]],[[511743,618128],[-2297,-915],[-819,958],[417,1545],[-146,2244],[-2215,1625],[-519,1089],[-1483,774],[-207,1050],[-1236,1551],[-57,1687],[-7683,10714],[-8895,12352]],[[486603,652802],[-6023,7672],[-4701,5897]],[[475879,666371],[0,2195]],[[475879,668566],[64,6293],[2709,3738],[1639,1633],[1277,-334],[374,1424],[2922,876],[1335,3012],[1793,1383],[1939,2174],[-580,782],[19,2750],[2248,1021],[240,1234],[1340,518],[3259,-243],[582,2247],[-1234,2425],[-470,2613],[-323,8491],[-1178,2087]],[[290876,504085],[-949,-96],[1008,-2562],[38,-2349],[-440,163],[-452,-3596],[-1442,-3565],[-1637,-2546],[-3282,-2482],[-1346,-2462],[-207,-2249],[-721,-3252],[-19,-1403],[-1084,-2536],[-707,372],[-854,2801],[-1392,942],[-678,-993],[-351,2335],[919,1136],[-404,2903]],[[594994,690286],[131,-677]],[[595125,689609],[1831,-10255]],[[602420,635036],[-6380,-3],[-8723,-3],[-5194,-4],[-6367,2],[-6367,2]],[[569389,635030],[0,42574],[-705,6331],[687,3116],[-336,3308],[827,1897]],[[617717,578574],[-1184,2464],[-520,1787],[-1117,1871],[-1648,3819],[-1522,1699],[-1916,625],[-927,-339],[-344,881],[-1583,-1207],[-1723,2535],[-869,-4166],[-872,1805],[-647,-1077],[-1389,-90]],[[601456,589181],[-271,5185],[264,700],[1089,6197],[-73,1946],[337,2573],[1117,16],[1032,2348],[1308,751],[989,2490]],[[495015,761882],[1066,-991],[-194,-1001],[3281,-1456],[717,-807],[1868,3],[182,921],[2032,-1477]],[[504739,756526],[-772,548]],[[504739,756526],[906,-888],[1720,-77],[1555,537]],[[479427,724985],[-272,2406],[1337,2720],[-890,2445],[959,3549],[-485,467],[-1009,3118],[1386,311],[629,3727],[-328,3946],[1989,3098],[-917,832],[-211,1599],[-1469,229],[-712,-873],[-2281,368],[32,1409],[-1567,-1141]],[[577812,857129],[-955,-2975]],[[576857,854154],[-403,147]],[[576732,848420],[54,9]],[[576786,848429],[-809,-2889]],[[575977,845540],[-2276,17],[-1504,1820],[-2445,1334],[-2190,-1143]],[[619229,569882],[-731,-2239],[506,-2478],[944,-1914],[836,-2966],[1501,-2331],[8209,-5859],[2778,0]],[[633272,552095],[-4320,-8885],[-4118,-9392],[-2643,228],[-2398,-1813],[-928,-2088],[-2132,-913],[-389,-949]],[[616344,528283],[-1842,-203],[-1266,1953],[-2564,-2498],[-966,-2342],[-3912,1141],[-3279,4519],[-2288,226],[-886,2123],[-50,3175],[-1324,879]],[[597967,537256],[-518,1071],[-1031,5849],[-1796,3350],[-1106,2638],[-1222,531],[-593,1130],[616,2636],[1997,279],[392,822],[-45,5209]],[[594661,560771],[593,3930],[-44,2389],[822,2086],[999,-91],[503,5639],[1343,4270],[1421,1120],[291,3227],[495,2103],[372,3737]],[[580460,913634],[-1375,-3161],[595,-1770],[1830,-758],[1765,-2211],[-2478,-4251],[2267,-5213],[534,-2425],[-767,-669],[-598,-3557],[1302,-1205],[98,-2364],[1099,-2046],[-1388,-1619],[3269,-3194],[981,-1912],[-690,-1883],[-4432,-6052],[-5257,-5983]],[[567098,894577],[-1122,2286],[658,3670],[-1445,3788],[474,2989],[-2379,2586],[-2181,768],[-3820,3059]],[[557283,913723],[2777,1385],[2192,-3263],[2536,-420],[1473,929],[3020,-1259],[2242,2351],[731,3925],[1427,1554],[3790,869],[3477,-2312],[533,-1175],[-1021,-2673]],[[348288,518502],[595,797],[574,2112],[-22,1898],[591,2674],[-1001,2752],[-274,2553],[-7,3131],[822,2047]],[[516081,798204],[1542,-507]],[[519503,776802],[-598,-1279],[964,-1831],[-1459,-1676],[1119,-2377],[-521,-1220],[345,-1367],[1860,-682],[-399,-2357]],[[520814,764013],[-644,-421]],[[503967,757074],[772,-548]],[[526641,510831],[4846,-191],[-19,6918]],[[482727,825162],[-1199,-177],[-1111,2071],[-849,-1701],[-2120,1737],[2499,4015]],[[620665,748254],[-1810,2705],[-893,-734],[-2657,460]],[[611050,761956],[478,888],[1383,-212],[2590,-1865],[2329,30],[3788,-2827],[485,-1069],[2538,1124],[2379,-1667],[-247,-1599],[2198,-1861]],[[499809,570571],[30,-2873],[1218,-2007],[-410,-4908],[822,-623],[-112,-3003],[-323,-546],[877,-2696],[-290,-939],[142,-4693],[-305,-2978],[588,-2360],[1250,-2152]],[[491349,534865],[76,235]],[[468362,578206],[-313,-1219],[547,-1085],[1034,1124],[710,-1811],[1118,1855],[1262,-1008],[1284,1262],[-104,1239],[980,-369],[624,-2843],[-10,-1476],[1151,-1700],[-713,-2077],[907,-267],[295,-3275],[669,-1632]],[[476426,549497],[-616,595],[-504,-2347],[-633,-278],[-962,1185],[263,1325],[-414,4186],[-695,1117],[-1431,-293]],[[471434,554987],[-1191,-888],[588,2087],[-528,3713],[-1431,3931],[-1959,90],[-1640,-775],[-706,-2895],[-756,-1599],[-736,-322]],[[458212,569531],[1002,3368],[1159,897],[1480,451],[-15,1621],[-586,998],[669,797],[-58,2140]],[[461863,579803],[1795,-239],[197,-924],[2002,-886],[2505,452]],[[453993,585214],[2924,-6],[1115,1338],[2174,-1917],[967,326],[361,-1234],[-1109,-589],[-2512,1900],[-375,-951],[-1467,-420],[-56,-999],[-2263,-14],[-317,-533]],[[453578,577913],[3158,803],[1052,1123],[4075,-36]],[[558233,746730],[1699,113],[983,1413],[1567,66],[1173,1290]],[[573113,751865],[844,-1865],[-817,-966],[1,-1684],[-811,-1349]],[[254921,597903],[-2078,-3474],[-683,-1639],[139,-1535],[-529,-1131]],[[251770,590124],[-2061,-3444],[26,-582]],[[243791,590891],[444,3133],[-311,1461],[1252,4439],[3582,15],[83,1886],[-815,1878],[-1942,3246],[1158,-21],[10,3342],[5077,-10]],[[341125,537589],[-378,-3130],[-1056,-173],[-954,-4853],[616,-2938],[787,-1914],[683,144],[261,-2929],[1404,-5014],[615,-559]],[[331272,535536],[-1763,4177],[689,1820],[-47,2846],[1188,437],[897,1049],[193,1117],[-855,457],[-239,1704],[571,1863],[1337,1424],[558,1495],[-517,1442]],[[817405,638260],[69,-246]],[[269007,593543],[-716,89],[-1256,-1266],[-1862,-954],[-897,1045],[-836,-1686],[-130,-1264],[-1607,-2769],[-704,1219],[-811,-1660],[-1115,-39],[64,-2666],[-968,-1908],[-773,-72]],[[256071,584100],[275,2450],[-1210,1034],[-921,-788],[-1772,3057],[-673,271]],[[551211,756860],[226,-751]],[[552514,776837],[644,-4357],[-361,-1939]],[[537716,774380],[2200,-210],[456,970],[869,-1054],[1368,-2],[-173,1574],[966,601],[31,2172],[2445,1773]],[[545878,780204],[2472,-3252],[1114,-952],[1531,-221],[1519,1058]],[[561477,791492],[1770,-1752],[299,-963]],[[563546,788777],[-1628,-1299],[-3475,-8801],[-2216,-792]],[[556227,777885],[-1975,276],[-1738,-1324]],[[545878,780204],[-1176,2151]],[[547631,789126],[1707,-1397],[2673,100],[188,1263],[3074,777],[1209,973],[434,1370],[2671,151],[876,-1269],[1014,398]],[[847411,448364],[-195,-316]],[[844380,449187],[165,186]],[[844545,449373],[683,-512],[450,1407]],[[845678,450268],[364,208]],[[846915,451584],[90,154]],[[847005,451738],[631,-1069],[-477,-428],[252,-1877]],[[890964,489271],[628,-15]],[[891592,489256],[0,-1148]],[[891592,488108],[4,-20988],[-314,-2334],[315,-979],[3,-13114]],[[891600,450693],[-144,200]],[[826595,529426],[-23,-50]],[[804524,516729],[69,-2445],[2367,-4460],[1200,920],[2311,-106],[857,853],[298,1752],[806,711],[1298,47],[126,-651],[1760,-1311],[779,1175],[1787,195],[791,3039],[-123,1602],[1091,1616],[-258,1883],[1023,1145],[310,2437],[7,2921],[910,2429],[3346,-69],[1316,-986]],[[720385,689001],[85,-152]],[[721990,687748],[22,-32]],[[725028,683425],[-1281,-1568],[-817,-2823],[-512,-3514],[3051,-2934],[1899,-2772],[375,277],[2070,-2339],[1547,-877],[1497,41],[728,672],[1442,-1141],[209,-1527],[1688,-1777],[765,585],[469,-1185],[1747,-387],[931,-826],[874,713],[754,-1156],[2132,414],[296,1746],[-492,2424],[349,4364]],[[770339,671893],[36,-1660],[-1211,-1741],[385,-3210],[-1035,1405],[-1677,-724],[-434,-1009],[-2157,-2663],[10,-3294],[-1606,-4726],[426,-1154],[-1152,-4306],[-459,-2639],[-2279,862],[299,-2014],[-137,-3255],[-559,-596],[-238,-1859],[232,-2121],[-549,-2112],[-765,754],[-317,-906]],[[689347,646059],[1553,636],[11,1783],[2308,44],[436,-595],[2308,1455],[470,-1068],[911,960],[10,1704],[-1099,4356],[-10,1446],[-1066,234],[-457,1206],[157,3326],[-1907,1973],[272,2193],[1601,3995],[720,1043],[927,-1754],[3147,1384],[1310,4676],[1560,1641],[1328,5365],[1188,942],[250,2026],[1223,2714],[815,836],[-320,895],[-22,3124],[638,1397],[1429,1135],[221,823],[-1877,1420],[15,1414],[-857,66],[-142,1321],[-859,1484],[433,1569],[-414,1666],[682,1196],[-824,170],[-389,1816],[420,1944],[942,663],[3914,-1554],[921,988],[1538,391],[1261,2216]],[[714023,712724],[2086,2287]],[[716109,715011],[31,-68]],[[719756,701565],[63,-322]],[[720045,697641],[8,-373]],[[649761,725957],[2308,938],[918,2374],[1397,1168],[1806,-156],[589,1043],[2091,-196],[395,-1341],[3056,-2082],[1055,266],[1350,-1024],[724,-1965],[1390,-1280],[774,-1927],[2162,29],[396,-6060]],[[669733,683211],[-724,-1598]],[[669009,681613],[1319,-2879],[846,-3442],[742,-1452],[2424,-2041],[1,-5639],[1122,13],[384,-758],[-381,-2719],[-2024,-619],[-556,-1209],[-1026,-679],[-559,-2805],[-224,-3357]],[[671077,654027],[-152,-40]],[[634851,682228],[-455,1586],[-1022,1395],[-12,3106],[-920,74],[0,2359],[418,2334],[-1274,3728],[-694,254],[-2067,2741],[-735,168],[92,1611],[-740,2253],[-1340,2139],[113,2632],[668,2271],[1266,1950],[-452,2349],[545,1756],[-1233,96],[-1005,1058],[-918,3026],[-739,3652]],[[624347,724766],[-566,3567],[-972,969],[609,2658],[-1132,6047],[1017,265],[549,2052],[640,-700]],[[633274,682349],[-850,668],[-1551,-795],[-580,-2511],[-1040,-2615]],[[629253,677096],[-486,-193],[-4555,770],[-5163,7712],[-2176,3466],[-4737,5087],[-3399,1099]],[[608737,695037],[283,1342],[-527,842],[-789,5208]],[[607704,702429],[5322,5687],[826,574],[577,2014],[60,3076],[383,2087],[-301,2565],[475,2614],[1033,489],[1584,3030]],[[617663,724565],[1155,1560],[2949,-879],[491,533],[746,-1987],[1343,974]],[[599409,698654],[-656,-2011]],[[598753,696643],[-995,823],[-659,-2213],[688,-2435],[-919,-2092],[1605,489]],[[598473,691215],[-77,-912]],[[598396,690303],[-61,-562]],[[598335,689741],[107,-581],[-830,-4216],[-464,-5130]],[[595125,689609],[645,2231]],[[597523,700720],[841,-48],[1272,2110]],[[599636,702782],[123,-2857],[-350,-1271]],[[538055,780323],[-894,-1532],[707,-500],[-175,-2032],[417,-1460]],[[608737,695037],[-509,-768],[-5566,-2982],[2838,-5874],[-963,-1106],[-456,-1886],[-1984,-764],[-775,-2197],[-1280,-1805],[-2957,966]],[[598473,691215],[301,1695]],[[598774,692910],[-21,3733]],[[599409,698654],[1624,-2062],[1240,-413],[5431,6250]],[[714023,712724],[-783,3269]],[[665127,772295],[-32,-131]],[[665095,772164],[-1906,1936]],[[663189,774100],[33,118]],[[722803,754670],[-801,1322],[-1197,263],[-1010,1885],[-1673,527],[-7596,404],[-428,-701],[-1633,532],[-2329,1990],[-1814,-1407],[-176,-3518],[-2055,1356],[-2601,1092],[-1556,-525],[-860,-2873]],[[697074,755017],[-1475,-1007],[-890,-1529],[-2863,-2687],[-1335,-2907],[46,-1282],[-1536,885],[-311,2294],[-3406,-103],[-586,4833],[-1359,59],[252,5841],[-825,-674],[-853,2568],[-1641,2395],[-1284,-969],[-3434,455],[-3380,-805],[-2304,4008],[-424,1334],[-2646,2687]],[[666820,770413],[22,2100]],[[662809,774782],[-59,-262]],[[662750,774520],[-391,14],[-6872,-3247],[5,-21758]],[[655492,749529],[-1200,-353],[-822,1158],[-960,2731],[-2175,2465],[-2419,-766],[-2100,-2521]],[[636756,779239],[-1728,1359],[-14,1181],[984,52],[-2360,5751],[-2271,-26],[-800,3220],[-954,757],[116,2330],[866,1735],[-590,1592],[528,2877],[929,2493],[1053,619],[2024,-3256],[1136,1095],[-606,3552],[1940,1416],[485,1372],[2080,1221],[1520,2605],[1529,-1504],[2740,1220],[667,-1182],[2131,4],[1954,-2175],[1690,-2696],[214,2002],[2264,-2348],[710,2],[1927,2473],[1445,270],[1196,-1044],[1102,1201],[1445,-166],[1457,-2187],[2580,-666],[396,1287],[2742,-615],[1243,981],[543,2184],[-617,1257],[-2495,1239],[-1109,1928],[1680,1033],[859,1445],[-492,2073],[681,1350],[2574,-171],[112,973],[-2265,1062],[933,1399],[-1543,583],[984,2533],[1653,-609],[3181,941],[3853,1653],[1935,-118],[887,1534],[2071,261],[4085,1215],[3567,3064],[3347,-1346],[1544,845],[1243,-4181],[-257,-2293],[676,-320],[2591,675],[375,-1824],[606,1007],[2935,-1213],[-776,-699],[-77,-2116],[1353,980],[1368,-783],[279,946],[3347,2717],[3279,1993],[-726,-2961],[3135,-3338],[2142,-4388],[2758,-6785],[1437,-4257],[1216,1017],[68,1404],[1193,582],[538,-1853],[1096,-1356],[2856,-73],[2398,1581],[1633,-1302],[1050,-3172],[1851,-1053],[840,-2737],[2470,-594],[1203,654],[1968,-3103]],[[616344,528283],[-1506,-4598],[-1048,-2293],[39,-21831],[1509,-4159],[30,-729]],[[608949,476917],[-3957,6031],[-524,1269],[98,2458],[-9967,11867]],[[594599,498542],[20,51]],[[594373,505981],[-1,1]],[[594372,505982],[1410,4908],[850,1118],[493,2445],[-165,4955],[-1272,4051],[-153,3128],[-633,720],[-525,2413]],[[594377,529720],[3590,7536]],[[704532,738430],[-2755,-373],[-1139,-1057],[-1178,403],[-932,1944],[-2048,-1128],[-348,896],[-3639,-235],[26,2629],[1833,1384],[1346,-906],[1407,1123]],[[697105,743110],[963,285],[1077,-797],[1219,1696],[629,-219],[2098,2277],[-1368,579],[-1267,1718],[-794,126],[-593,2051],[-713,-2400],[-1739,749],[-1671,1830],[1836,2655],[1074,849],[-782,508]],[[785927,574073],[-548,2269],[52,1994],[-711,1444],[-499,5154],[631,271],[1005,3264],[807,1161],[4388,564],[819,-1187],[304,704]],[[792175,589711],[464,-1402],[503,276],[1036,-1373],[612,738],[-406,1742],[1453,1393],[884,-1561],[1943,2313]],[[798664,591837],[-522,-3427],[761,-4081],[-362,-2414],[223,-2905],[-450,-1656],[-651,98],[-635,-1182],[-1435,-765],[-4,-1485],[-1129,357],[-429,-729],[13,-2018],[866,-1671],[-11,-1288],[-1136,1156],[-2035,-611],[-477,-2088],[-1179,-730]],[[851760,728554],[1487,3097],[2416,23],[932,1866]],[[559895,755010],[-1396,-451],[-1371,-1763]],[[555733,756786],[778,1663]],[[556511,758449],[1164,2552],[1743,-3005],[1006,-484],[-529,-2502]],[[634562,673818],[-2142,-58],[-662,2704],[-2505,632]],[[783687,637302],[1263,-2814],[314,-1435],[706,114],[-273,-2461],[704,-2217],[1472,-1153],[1160,1446],[1475,-1745],[-598,-1216],[697,-396],[859,-2112],[-1060,-2414],[-1430,383],[-377,-1986],[2278,-3179],[1196,-903],[-169,-1190],[1035,-1753],[647,-2467],[2253,-4643],[538,-2933],[1945,-2465],[-640,-1425],[1354,-3242],[-480,-1631],[108,-1628]],[[792175,589711],[812,1089],[104,4922],[303,2009],[-600,1703],[-997,1024],[-824,2887],[182,3867],[-1371,3054],[-1037,2981],[-1836,530],[-658,-2251],[-1207,-1156],[-1432,2235],[-2767,-4331],[-546,618],[568,2664],[-174,2213],[655,3377],[-207,3384],[-1629,-287],[-632,1518],[404,1970],[-626,1761],[-543,-410]],[[778117,625082],[353,2451],[876,561],[533,2889],[1062,1510]],[[599933,709876],[1269,-93],[422,-2324],[-1785,-3280],[-203,-1397]],[[468034,545635],[666,1931],[1723,3121],[213,1847],[503,512],[295,1941]],[[569389,635030],[-2,-11809],[-2776,-39],[0,-2958]],[[566611,620224],[-5322,5606],[-6655,7008],[-3992,4205],[-6242,6574],[-2792,-2660]],[[541608,640957],[-2079,-2238],[-2082,3328],[-4203,2001]],[[526440,683810],[1046,935],[892,2346],[-281,4032],[1976,3654],[1885,1973],[-1,4552]],[[579824,326379],[-958,-270],[-1038,-2931],[-737,251],[-1012,1683],[-936,3862],[675,857],[1225,3432],[2472,2123],[1877,-3010],[248,-1066],[-813,-3847],[-1003,-1084]],[[565235,824281],[-87,1206],[-1909,1264]],[[563239,826751],[181,2854],[-734,1307],[-1374,27],[-2324,1188]],[[558988,832127],[1,41]],[[558461,836902],[2884,1994],[4801,-459],[855,-385],[2174,793],[463,-1171],[1433,-416],[2799,-2741]],[[575977,845540],[1236,-1251],[-437,-2793],[1288,-1777],[124,-2387]],[[475879,668566],[-373,0],[63,-3174],[-1718,-191],[-894,-1348],[-1434,0],[-1865,886],[-1305,-753],[152,-1481],[-1056,-3135],[-868,-434],[-1112,-7111],[-1750,-2545],[-694,-2488],[-1278,-1128],[-695,-2251],[-556,-6520],[-1138,-2662],[-583,-2430],[-2528,237],[-3478,-415]],[[452769,631623],[199,2840]],[[463177,667252],[223,1310]],[[578367,773986],[-313,3094],[402,2835],[-479,3122],[-2042,3919],[-989,3053],[-1005,621]],[[573941,790630],[2584,1290],[1693,-1419],[2595,-1556],[186,-3080],[1082,-1236],[63,-1676],[849,-800],[-111,-2835],[-1921,1046],[-608,-609],[57,-2217],[-2043,-3552]],[[187598,692927],[3952,-2631]],[[191550,690296],[7854,31]],[[199404,690327],[7,2665]],[[199411,692992],[4885,-54],[549,-1336]],[[204845,691602],[2722,-4369]],[[207567,687233],[825,-955],[1319,-5737]],[[209711,680541],[1093,-1727]],[[210804,678814],[2369,-2281]],[[213173,676533],[1089,1522]],[[214262,678055],[364,2286]],[[214626,680341],[1292,1347],[1947,-368],[1471,-2067]],[[219336,679253],[1056,-2321]],[[220392,676932],[1008,-4389]],[[221400,672543],[2196,-4617]],[[223596,667926],[136,-2913]],[[223732,665013],[790,-2918]],[[224522,662095],[178,-694],[2848,-2266],[2011,-1149]],[[229559,657986],[590,539]],[[174644,697459],[6675,1079]],[[181319,698538],[-308,-1227]],[[181011,697311],[6587,-4384]],[[559895,755010],[2171,394]],[[511743,618128],[19,-12717],[-314,-3783],[-679,-3570],[-1035,-2363],[-6123,-498],[-945,-1691],[-2063,-447]],[[468362,578206],[-2,3185],[-680,2535],[-546,-320],[-619,1879],[98,3398],[-581,1493],[-145,2076]],[[465887,592452],[488,-377],[644,1480],[313,2550],[847,1184],[1409,-2810],[699,1609],[2098,-290],[2123,725],[10179,1],[424,4660],[-747,1693],[-1105,20579],[-1576,29341],[4920,5]],[[778117,625082],[-645,639],[-1198,-364],[119,-1039],[-1336,-864],[-289,-1593],[-1882,-487],[-623,348],[-550,-1715],[-308,-3129],[133,-1843],[-747,-750],[409,-1208],[446,-3608],[1795,-4180],[109,-1443],[944,-3266],[-627,-771],[-75,-3834],[-1040,-1182],[153,-2307],[900,-2694],[1010,-1837],[564,-1974],[-81,-3633],[825,-3291],[584,-4543],[-1180,-4004],[-1202,-2633],[-152,-2787]],[[553317,762628],[992,-1902],[2202,-2277]],[[553773,755249],[-181,-138]],[[553378,754483],[347,-292]],[[743928,795977],[1051,1713],[2266,126],[1793,1450],[-28,1099],[2809,1892],[4721,3802],[2079,-1541],[3189,-282],[1010,-3156],[1380,-523],[2057,458],[3149,-770],[619,-901],[2486,2057],[489,2697],[-1262,2679],[338,2151],[2628,4555],[2857,-2143],[1521,-174],[2533,-1620],[2029,-588],[492,-4553],[1097,-1172],[2636,-1472],[4864,1984],[2318,-1001],[1370,48],[1450,-1915],[1985,-383],[238,-1960],[1612,-1606],[1731,71],[2675,-974],[1744,-25],[1414,1124],[2064,405],[2710,1136],[302,1073],[3146,2827],[1240,-241],[1475,-1687],[1232,-405],[1158,772],[1524,-1108]],[[591350,345650],[-2148,58]],[[589202,345708],[-146,4865],[-311,359]],[[588745,350932],[104,8869],[-517,3368],[-706,2428],[-716,6400]],[[586910,371997],[3009,6323],[296,3684],[542,1166],[928,3806],[-637,2873],[-169,2291],[768,3807],[-125,9758],[-1958,1562],[-843,118],[-700,1272],[-1254,1129],[-2218,168],[-116,2086]],[[584433,412040],[-456,3868],[1226,1014],[7024,4773]],[[592227,421695],[1207,-3286],[1934,945],[479,-1123],[99,-4142],[-813,-3497],[409,-1846],[2486,-5319],[-342,3180],[531,2368],[1103,605],[382,6911],[-127,1308],[-1666,4587],[-1075,2219]],[[596834,424605],[3,366]],[[597102,435195],[7,928]],[[597109,436123],[1865,-23],[429,765],[1128,-1290],[909,-270],[983,859],[1390,-825],[2232,2558],[876,-797],[842,1092],[1463,630],[1853,1788],[1319,2111]],[[465887,592452],[-1606,2569],[-1936,5341],[-869,24],[-1199,2560],[-1918,573],[-2160,-1137],[-1308,274],[-823,-4105]],[[452643,627982],[233,3099],[10967,28],[-217,6885],[-200,1523],[374,1464],[1143,1606],[1658,1163],[20,14976],[9261,0],[-3,7645]],[[596829,424051],[5,554]],[[592227,421695],[-583,-52],[-889,2440],[821,2283],[150,3522],[1045,834],[-404,2235],[-72,3422],[426,2234],[-329,1566],[1105,1795],[-362,2108],[-604,1165],[-321,2439],[-766,1297]],[[591444,448983],[1391,-1188],[1480,-297]],[[778108,542882],[160,1362],[1714,-1455],[721,-1088],[-199,-2794],[366,-795],[1229,1605],[883,-488],[630,2470]],[[826676,529600],[-81,-174]],[[816815,531927],[28,-1]],[[564946,400206],[2484,945],[1826,-369],[907,-1482]],[[555501,357928],[0,-21769],[-859,-312],[-1416,-2576],[-897,412],[-2044,-15],[-1819,1028],[-173,2044],[-915,1908],[-835,-2494],[-856,-980]],[[541608,640957],[537,-6364],[26,-2362],[1182,-3371],[-56,-1309],[1005,-2549],[-594,-2364],[-724,-17748],[-3074,-6862],[-2554,-8113],[439,-4006]],[[537795,585909],[-785,-200],[-1858,-2039],[-533,-1380],[-2920,1540],[-1258,106],[-2151,-601],[-1580,-2722],[-2403,578],[-1821,2269],[-851,277],[-2033,-2001],[-702,637],[-1161,2938],[-2484,1595],[-695,-685],[-1162,15],[-1878,-1789],[-554,-4045],[-837,-1452],[-142,-4939]],[[537795,585909],[1271,-3709]],[[516367,805398],[-552,132]],[[585749,918146],[-25,-1452],[-1759,565],[-3505,-3625]],[[557283,913723],[-1404,-95],[563,-1581],[-971,-2356],[-4420,1221],[-1283,-3540],[-1645,823],[-3325,-4017],[767,-2197],[-2724,-3348],[169,-1089],[-2613,-1047],[-176,-4904],[-2304,-4266],[1187,-696],[-325,-2666],[-1836,360],[-1770,-796],[-1840,-3844],[606,-1724],[-288,-2421],[525,-1815],[-412,-3346],[2015,-2183],[-549,-1811],[-1080,-260],[818,-3270],[-285,-2038],[-2237,-3048],[-105,-3947],[-707,654]],[[647459,603350],[-990,3862],[-2087,10047]],[[644382,617259],[8332,5923],[1844,11884],[-1265,4160]],[[671509,653647],[-432,380]],[[307320,412833],[-113,108]],[[307956,408551],[363,-118]],[[308196,406812],[33,-40]],[[892036,450086],[-436,607]],[[891592,488108],[0,1148]],[[891592,489256],[365,-10]],[[554456,827357],[1677,-227],[7106,-379]],[[565570,809933],[1390,-3987],[-370,-2577],[-725,-194],[-2951,-4966],[446,-3071],[-753,308]],[[562607,795446],[-2497,2010],[-2844,-120],[-2387,-1110],[-875,2330],[-812,-1171],[-881,656]],[[539607,823035],[636,-341]],[[539863,823801],[-381,71]],[[539476,824343],[6,-471]],[[862574,756716],[445,-1380]],[[642410,650501],[-838,-197],[-449,1276]],[[578367,773986],[1523,-1281],[1786,1099],[840,-947]],[[563069,766802],[-576,2775],[-1124,-973],[-2042,1895],[372,1543],[-1993,2145],[2,1573],[-1481,2125]],[[563546,788777],[905,815],[2709,-1058],[1114,148],[874,-1263],[1585,1143],[1941,484],[1267,1584]],[[558952,831200],[36,927]],[[558054,832260],[-112,-611]],[[0,890205],[0,1449]],[[0,925312],[0,1133]],[[0,927721],[0,818]],[[999999,913406],[0,-23201]],[[866732,772708],[-53,-123]],[[868915,771622],[0,87]],[[606150,783708],[203,2771],[1703,1754],[2321,-62],[617,2513],[-870,1909],[956,1541],[-840,928],[1065,1139],[-109,2350],[-695,-147],[-1683,1682],[-712,-185],[-1833,1349],[-588,-784],[-1733,2911],[-2232,-1198],[-3355,1957],[-277,2988],[-688,945],[-2306,240],[-314,2579],[769,599],[-1841,3344],[-3409,-214],[-625,-1153],[-1443,-78]],[[576786,848429],[98,16]],[[576897,854139],[-40,15]],[[683568,913720],[921,-525]],[[584749,498394],[841,-2938],[45,-4593],[-764,-366]],[[580160,490226],[146,78]],[[581248,494433],[-78,295]],[[582158,496495],[981,-486],[1194,2342],[416,43]],[[452751,631365],[18,258]],[[644382,617259],[-7737,-2221],[-2834,-2751],[-1646,-4198],[-383,-1993],[-1295,-939],[-815,1867],[-1033,-221],[-2510,524],[-718,638],[-2756,-171],[-664,-438],[-1386,1135],[-631,-929],[-72,-3969],[-1016,-1882]],[[594661,560771],[-520,4],[189,2270],[-186,2095],[-2000,3858],[-275,4392],[351,3708],[-1326,34],[41,-1264],[-1846,-18],[731,-1722],[191,-3900],[-1309,-2341],[-772,-2615],[-1195,-2500],[-1348,-335],[-2046,3168],[-1104,-1258],[-368,-1756],[-1315,-939],[-431,-1683],[-2210,15],[-453,1606],[-2254,84],[-1453,-522],[-1833,4011],[-259,1290],[-2031,-751],[-783,-3075],[-703,-5260],[-1069,-1311]],[[563500,569410],[173,2519],[-1017,1924],[-567,5870],[-1464,771],[1119,3194],[-335,2374],[1118,2352],[-357,2507],[804,1019],[726,2604],[5,2198],[475,1004],[2440,460],[-9,22018]],[[594377,529720],[-1352,-2756],[-1367,741],[-1837,-1031],[-499,-1055],[-995,1627],[-883,-724],[-910,623],[-872,-1747]],[[635940,571417],[-2,-10703],[-2666,-8619]],[[562607,795446],[-1130,-3954]],[[549900,856389],[43,-180]],[[589202,345708],[-328,130],[-101,-2893],[-1358,61],[-1128,1086],[-748,2062],[25,2078],[1122,3377],[578,574],[1481,-1251]],[[599701,717503],[725,-490],[616,1999],[726,372],[-276,1323],[337,2045],[2160,-943],[2098,1530],[1597,-1235],[1639,-69],[1833,856],[1915,1610],[2249,-51],[2092,1110],[251,-995]],[[688219,724942],[154,1865],[1332,3234],[108,1214],[-792,2556],[155,1735],[-1186,275],[-908,1384],[1026,2247],[2067,-502],[1288,3553],[967,366],[-191,2183],[577,1366],[830,-831],[2024,2171],[860,-1681],[-1023,-1695],[751,-1495],[847,223]],[[655492,749529],[2891,-348],[-150,3513],[346,518]],[[658579,753212],[-98,-1010]],[[660075,754430],[-26,79]],[[660049,754509],[1142,1924],[1269,-1012],[-929,1844],[1216,891],[2394,-2837],[1131,-26],[290,-2019],[638,-711],[-285,-2577],[1015,-1053],[2427,-157],[467,479],[1128,-1080],[2077,-7318],[4200,-5361],[4028,-4236],[679,178],[2145,-1994],[-298,-3458]],[[844545,449373],[448,506]],[[844993,449879],[685,389]],[[847805,449005],[-394,-641]],[[847005,451738],[291,494]],[[588280,498780],[5,-46]],[[594254,497680],[345,862]],[[597109,436123],[-62,678]],[[591444,448983],[-1352,1488],[-1363,606],[-1133,2019],[-969,612]],[[586627,453708],[-6,44]],[[582337,478120],[15,207]],[[584749,498394],[930,386],[2601,0]],[[594372,505982],[-81,24]],[[588243,499173],[37,-393]],[[582342,501717],[624,1207]],[[582730,503997],[-277,8]],[[586953,517904],[-112,-392]],[[352309,311155],[-25,-42]],[[351267,306014],[29,-49]],[[251173,789100],[-75,-59]],[[280385,761145],[23,13]],[[289481,768262],[-31,38]],[[311664,775792],[-173,-93]],[[312640,774914],[-1,5]],[[138294,830715],[-1003,-1519]],[[663118,773843],[71,257]],[[665095,772164],[-147,-601]],[[666819,770377],[1,36]],[[660049,754509],[-189,580]],[[658593,753357],[-14,-145]],[[662750,774520],[-4,-21]],[[581568,373230],[829,282],[2214,-1082],[1267,227],[1032,-660]],[[586662,453459],[-35,249]],[[584433,412040],[-2524,-318],[-1700,-2010],[-319,-2939]],[[579890,406773],[-220,141]],[[575057,398323],[-669,-492],[-1240,665],[-1306,-134],[-1679,938]],[[584936,456017],[-10,-250]],[[575633,399537],[498,1027]],[[194446,794974],[-11287,0]],[[183159,794974],[-1817,3409],[182,2469],[-384,2214],[-1158,1154],[-2198,3412],[-1879,3917],[-663,-456],[-1133,2619],[-1142,1346],[-1360,18],[-273,1708],[-1344,2838],[-2555,1459],[-771,2631],[2,36479]],[[166666,860191],[12153,0],[5208,0],[10417,0]],[[194444,860191],[2,-65217]],[[146674,804734],[4763,-1918],[2332,-5262],[1798,-1212]],[[155567,796342],[1385,-3802],[-271,-1473]],[[156681,791067],[-4239,2531]],[[152442,793598],[753,1585]],[[153195,795183],[-1696,-517]],[[151499,794666],[-511,1450]],[[150988,796116],[-1665,1521],[-289,1293]],[[149034,798930],[-2506,2827]],[[146528,801757],[-1706,-61]],[[144822,801696],[-116,1881]],[[144706,803577],[1164,-240],[58,1057]],[[145928,804394],[-1647,-501]],[[144281,803893],[-798,1456]],[[143483,805349],[1189,689]],[[144672,806038],[2002,-1304]],[[134017,819872],[828,-2926]],[[134845,816946],[-375,-732]],[[134470,816214],[1025,-2515],[-2622,3729],[60,1281],[-1119,819]],[[131814,819528],[2203,344]],[[142910,818356],[118,-2495]],[[143028,815861],[-470,-1357],[-187,2807]],[[142371,817311],[-427,-530],[-771,2038]],[[141173,818819],[401,1551],[1336,-2014]],[[139308,819707],[-1857,2230]],[[137451,821937],[1590,-639]],[[139041,821298],[267,-1591]],[[131512,825393],[1448,-552]],[[132960,824841],[1295,634]],[[134255,825475],[-953,-5192]],[[133302,820283],[-2316,1437]],[[130986,821720],[-577,1603]],[[130409,823323],[11,2256]],[[130420,825579],[1092,-186]],[[138819,835824],[-202,1310]],[[138617,837134],[-4105,2900]],[[134512,840034],[-691,-52]],[[133821,839982],[-576,2586],[-4971,9944],[-3119,3456]],[[125155,855968],[-298,1719]],[[124857,857687],[-1180,1273],[-2349,-1117]],[[121328,857843],[-713,-2681],[-2389,-1476]],[[118226,853686],[-430,1914]],[[117796,855600],[-4064,4594]],[[113732,860194],[7956,-3],[7932,0],[5287,0],[7932,0],[5287,0],[7932,0]],[[156058,860191],[10608,0]],[[183159,794974],[-5516,0]],[[177643,794974],[-2753,0]],[[174890,794974],[-7396,0]],[[167494,794974],[-8574,0]],[[158920,794974],[-724,0]],[[158196,794974],[-795,2762]],[[157401,797736],[-1545,209]],[[155856,797945],[-35,2883]],[[155821,800828],[-659,-1117]],[[155162,799711],[-1780,1348]],[[153382,801059],[-768,2925]],[[152614,803984],[-3864,212],[1534,798]],[[150284,804994],[-1721,237]],[[148563,805231],[-319,1130]],[[148244,806361],[-1182,-282]],[[147062,806079],[-1807,1681]],[[145255,807760],[176,1939]],[[145431,809699],[-623,1758]],[[144808,811457],[214,3046]],[[145022,814503],[-863,-2968]],[[144159,811535],[-708,2195]],[[143451,813730],[699,4431]],[[143429,817681],[-797,2476],[-1191,732]],[[141441,820889],[-93,1622],[1590,-1307]],[[142938,821204],[-1159,2494]],[[141779,823698],[-903,-2657]],[[140876,821041],[-776,-838],[-2144,2799]],[[137956,823002],[813,2426],[-1076,1704],[2415,6170]],[[140108,833302],[-1178,-614]],[[138930,832688],[-111,3136]],[[252921,841529],[-12978,-18385],[-4259,-5483],[-5,-20456]],[[235679,797205],[-18,-2238],[-5733,8]],[[229928,794975],[-4397,-1],[-7107,0]],[[218424,794974],[-987,22745],[-773,17686],[-2,24786]],[[216662,860191],[11248,0],[8834,3]],[[236744,860194],[-45,-4347]],[[236699,855847],[319,-2357]],[[237018,853490],[1073,-913]],[[238091,852577],[-125,-2480]],[[237966,850097],[767,2741]],[[238733,852838],[2161,-22]],[[240894,852816],[2329,-8777]],[[243223,844039],[-794,-2022]],[[242429,842017],[4484,1823]],[[246913,843840],[1442,-99]],[[248355,843741],[2226,-1441]],[[250581,842300],[2340,-771]],[[322136,777316],[-788,-1048]],[[321348,776268],[-4361,-3630]],[[316987,772638],[-2744,-922]],[[314243,771716],[-701,605]],[[313542,772321],[-966,631]],[[312576,772952],[64,1967],[-976,873]],[[311664,775792],[-17,7865],[-1191,1559],[-1647,-845]],[[308809,784371],[-625,541]],[[308184,784912],[1874,1744],[3,2015],[2125,410],[689,-850],[1835,993]],[[314710,789224],[2375,-660]],[[317085,788564],[508,-1273]],[[317593,787291],[1846,893],[830,-723]],[[320269,787461],[-581,-2111]],[[319688,785350],[-1130,-1585]],[[318558,783765],[1355,-239],[-207,-1024]],[[319706,782502],[1012,-3837],[1737,-441]],[[322455,778224],[-319,-908]],[[345948,810042],[-1590,-1233],[641,-1748],[-2483,-5769]],[[342516,801292],[-356,-2642]],[[342160,798650],[1598,2823]],[[343758,801473],[238,-888]],[[343996,800585],[1754,338]],[[345750,800923],[-1417,-1733]],[[344333,799190],[-131,-1498],[2445,179]],[[346647,797871],[-104,-1672]],[[346543,796199],[2153,1954],[2361,-1232]],[[351057,796921],[128,-1069]],[[351185,795852],[-1633,-2094],[1286,-640]],[[350838,793118],[-1027,-1546]],[[349811,791572],[2807,1423]],[[352618,792995],[-218,-1524],[-1316,-1151]],[[351084,790320],[-700,-2418]],[[350384,787902],[716,-812]],[[351100,787090],[892,1987],[1158,683]],[[353150,789760],[-860,-2725]],[[352290,787035],[147,-1173],[945,1861],[357,-1302]],[[353739,786421],[-1156,-5144],[-1518,-6]],[[351065,781271],[-55,2711]],[[351010,783982],[-1493,-1524]],[[349517,782458],[846,3001]],[[350363,785459],[-896,2801]],[[349467,788260],[-1030,-2871]],[[348437,785389],[-817,58]],[[347620,785447],[-1275,-2839]],[[346345,782608],[-1314,-228],[-138,1210],[964,528]],[[345857,784118],[1728,2431],[-1380,533],[-190,-946],[-1188,171]],[[344827,786307],[12,1712]],[[344839,788019],[-1010,-875]],[[343829,787144],[-2031,-574]],[[341798,786570],[-3835,606]],[[337963,787176],[-2177,-629]],[[335786,786547],[-431,2517]],[[335355,789064],[2616,3120]],[[337971,792184],[-1072,450],[868,2880]],[[337767,795514],[1177,861]],[[338944,796375],[-100,1854],[1994,6850],[1522,3414]],[[342360,808493],[2013,1739],[1575,-190]],[[341388,809491],[-2,3304],[-11295,-1],[-6776,0],[-1264,2706],[1811,1369],[-728,2441],[-1408,-1692],[133,-3724],[-444,-2714],[-2352,2525],[-2496,-269],[-1188,1375],[453,3291],[-2132,-841],[361,3400],[-1442,1614],[-834,2561],[683,1201],[-302,1601],[1306,653],[-798,2486],[2184,-1408],[-342,3138],[3051,-3456],[3708,-111],[1638,-676],[667,3290],[1109,1021],[-2479,4488],[-299,3517],[580,1175],[793,4957],[-1188,351],[-906,2275],[1454,1721],[-619,1078],[1438,533],[-3487,1171],[1153,410],[-143,3137],[-1037,72],[424,2165],[-596,853],[738,1519]],[[320515,861997],[-428,-1741]],[[320087,860256],[1347,308]],[[321434,860564],[2066,-4332]],[[323500,856232],[94,-1290]],[[323594,854942],[1756,-2623]],[[325350,852319],[-1433,-1302],[2172,259]],[[326089,851276],[-1036,-2388]],[[325053,848888],[1374,360]],[[326427,849248],[1631,-1734]],[[328058,847514],[-191,-1478],[-1353,-888]],[[326514,845148],[1677,-478]],[[328191,844670],[-351,-790]],[[327840,843880],[1788,-1407],[-105,-1953]],[[329523,840520],[-1919,107]],[[327604,840627],[1770,-2004]],[[329374,838623],[-68,-2163],[1716,-223]],[[331022,836237],[1364,-1026]],[[332386,835211],[413,-1800]],[[332799,833411],[-1180,-2492]],[[331619,830919],[2384,1477]],[[334003,832396],[2116,-949]],[[336119,831447],[602,-1843]],[[336721,829604],[2272,222]],[[338993,829826],[1550,-1809]],[[340543,828017],[-2075,-1304]],[[338468,826713],[-1338,-1782]],[[337130,824931],[-3305,-1274]],[[333825,823657],[-1407,-3367]],[[332418,820290],[2798,2237]],[[335216,822527],[1861,1980],[2011,744]],[[339088,825251],[-733,738]],[[338355,825989],[2156,-387]],[[340511,825602],[780,-2198]],[[341291,823404],[-1089,-1138],[543,-774]],[[340745,821492],[1363,1602]],[[342108,823094],[2030,-901]],[[344138,822193],[867,-2225]],[[345005,819968],[-117,-4172]],[[344888,815796],[403,-2191]],[[345291,813605],[-3903,-4114]],[[322136,777316],[2050,-1544]],[[324186,775772],[1645,-68],[605,-703]],[[326436,775001],[1465,1460]],[[327901,776461],[1287,-1074]],[[329188,775387],[1280,-2341]],[[330468,773046],[-4118,-2655]],[[326350,770391],[-2201,-1192]],[[324149,769199],[-827,242]],[[323322,769441],[-437,-1166]],[[322885,768275],[-662,938],[-2397,-4603]],[[319826,764610],[-2432,-1819]],[[317394,762791],[-1077,1499]],[[316317,764290],[73,3279]],[[316390,767569],[1610,2165]],[[318000,769734],[-380,163]],[[317620,769897],[3683,3252]],[[321303,773149],[-65,-1013]],[[321238,772136],[2739,1342],[-4291,60]],[[319686,773538],[1662,2730]],[[331391,775898],[539,2552]],[[331930,778450],[1778,-263]],[[333708,778187],[103,-1152]],[[333811,777035],[-1550,-1839]],[[332261,775196],[-2494,-479]],[[329767,774717],[-587,2177],[1736,5067]],[[330916,781961],[1283,1226]],[[332199,783187],[212,-1397],[-681,-3528],[-1467,-1348],[128,-1430],[1000,414]],[[164773,916863],[10,-9267],[10642,-6944],[4258,-2777],[7806,-5092],[5096,-31],[3202,-3521],[1510,-733],[12107,-2039],[7265,-1224],[-7,-25044]],[[216662,860191],[-12497,0],[-9721,0]],[[156058,860191],[-2168,3915],[46,1723],[-2210,-955],[-4198,24],[-508,3401],[-2558,2023],[-1576,2404],[-1967,292],[161,2057],[-1411,1885],[248,1402],[-1401,1233],[803,1241],[-2265,2753],[-1240,2668],[-4151,2518],[686,1260],[-1134,1103],[1576,2209],[-1164,2556],[-2708,-394],[-129,3572],[-1167,2600],[-5751,2],[-120,3019],[-768,1376],[6,6805]],[[120990,912883],[3004,-1178]],[[123994,911705],[-1429,1307],[514,2336]],[[123079,915348],[4976,1286],[-762,-1632]],[[127293,915002],[2808,1073]],[[130101,916075],[898,1283]],[[130999,917358],[4733,1519]],[[135732,918877],[1772,1400]],[[137504,920277],[2361,-862]],[[139865,919415],[-3643,-2167],[-2508,-489]],[[133714,916759],[-4211,-3927]],[[129503,912832],[1868,-425],[-35,1566]],[[131336,913973],[2585,2089],[2153,-19]],[[136074,916043],[119,-1301],[1714,2648],[3455,1339]],[[141362,918729],[384,-1004]],[[141746,917725],[3576,3246],[-853,1857]],[[144469,922828],[2368,-1982]],[[146837,920846],[1462,-3015],[3403,-2258]],[[151702,915573],[516,2842]],[[152218,918415],[2102,1669],[889,-2492]],[[155209,917592],[-837,-1840]],[[154372,915752],[2268,-13]],[[156640,915739],[1622,2564]],[[158262,918303],[4151,-203]],[[162413,918100],[2360,-1237]],[[194439,937095],[5,-17659],[-8004,0],[-11594,6],[552,-2089]],[[175398,917353],[-774,2669]],[[174624,920022],[7063,1258],[5429,-517]],[[187116,920763],[2793,495]],[[189909,921258],[-5902,2263]],[[184007,923521],[-6204,-619]],[[177803,922902],[-4434,256]],[[173369,923158],[-1880,1533],[1249,1601],[5341,1323]],[[178079,927615],[846,975]],[[178925,928590],[-5934,-923]],[[172991,927667],[-54,1592],[-3127,163]],[[169810,929422],[-213,1770]],[[169597,931192],[2032,1642]],[[171629,932834],[-448,1607]],[[171181,934441],[2286,1760]],[[173467,936201],[8093,3209]],[[181560,939410],[1318,-609]],[[182878,938801],[152,-2422]],[[183030,936379],[-969,-1663]],[[182061,934716],[2661,676]],[[184722,935392],[811,1698]],[[185533,937090],[5384,-1584]],[[190917,935506],[-1737,-2119]],[[189180,933387],[4693,1808]],[[193873,935195],[-1266,2056],[1832,-156]],[[167398,943794],[1680,-502]],[[169078,943292],[1633,1284]],[[170711,944576],[2859,-77]],[[173570,944499],[5567,-3631]],[[179137,940868],[177,-1066]],[[179314,939802],[-9763,-4471]],[[169551,935331],[-1240,-1918],[-2144,-875],[-1221,-4189]],[[164946,928349],[-3139,-361]],[[161807,927988],[-3297,-2114],[-2976,3493]],[[155534,929367],[-4875,2725]],[[150659,932092],[2154,2668]],[[152813,934760],[419,2893]],[[153232,937653],[2887,4099]],[[156119,941752],[-2498,3437]],[[153621,945189],[9391,1077]],[[163012,946266],[4869,-1760]],[[167881,944506],[-483,-712]],[[168348,952708],[4561,2933],[-1599,-3156],[-2962,223]],[[194437,952245],[0,-4077]],[[194437,948168],[-6992,-2572]],[[187445,945596],[-2762,79]],[[184683,945675],[-1718,1990]],[[182965,947665],[3601,1240],[3236,261]],[[189802,949166],[1770,1229],[-7438,-938]],[[184134,949457],[-847,2167]],[[183287,951624],[-1209,-2052]],[[182078,949572],[-3547,-710]],[[178531,948862],[-5198,1799]],[[173333,950661],[1238,1192]],[[174571,951853],[2992,119]],[[177563,951972],[2654,1260],[-5287,-617]],[[174930,952615],[1909,1655]],[[176839,954270],[-756,1141],[2859,2156]],[[178942,957567],[3852,83]],[[182794,957650],[1029,-1449]],[[183823,956201],[3128,-30],[4569,-3870]],[[191520,952301],[2917,-56]],[[179024,963052],[-2040,-1550]],[[176984,961502],[181,-2907],[-2593,-1854]],[[174572,956741],[-2406,880]],[[172166,957621],[666,2001]],[[172832,959622],[-2809,-1607]],[[170023,958015],[-1047,-2290],[-986,1266],[-1082,-2852]],[[166908,954139],[-3074,957]],[[163834,955096],[-3836,-451]],[[159998,954645],[97,2708],[2088,238],[7011,5116]],[[169194,962707],[5030,50],[2545,1359]],[[176769,964116],[2255,-1064]],[[183799,965371],[-3325,1261]],[[180474,966632],[1941,652]],[[182415,967284],[1384,-1913]],[[193893,964006],[-4872,-1067]],[[189021,962939],[-3367,1102],[-63,2263]],[[185591,966304],[8843,1052],[-541,-3350]],[[190458,968527],[-4765,716],[7619,2064],[1122,-2577],[-3976,-203]],[[275745,817216],[-3632,1793]],[[272113,819009],[533,807]],[[272646,819816],[1977,116]],[[274623,819932],[1122,-2716]],[[280734,838063],[-2959,-1979]],[[277775,836084],[2024,3959]],[[279799,840043],[935,-1980]],[[310717,863514],[897,-667]],[[311614,862847],[-1153,-1236],[256,1903]],[[319909,868276],[-1665,1681]],[[318244,869957],[1785,75],[-120,-1756]],[[279041,874472],[614,-2284]],[[279655,872188],[-958,-2261],[-1656,1029]],[[277041,870956],[677,3109]],[[277718,874065],[1323,407]],[[304619,875284],[-1192,285]],[[303427,875569],[-1024,1666]],[[302403,877235],[1923,-856]],[[304326,876379],[293,-1095]],[[272221,877686],[-315,-1789]],[[271906,875897],[-2506,-2620]],[[269400,873277],[-1897,-294]],[[267503,872983],[-557,1873]],[[266946,874856],[1453,2538]],[[268399,877394],[3822,292]],[[284615,879658],[-1122,-1024]],[[283493,878634],[-1569,1996]],[[281924,880630],[1751,115],[940,-1087]],[[285098,881443],[642,556],[1267,-1708],[-1909,1152]],[[264112,891353],[853,1103],[7118,-4757]],[[272083,887699],[1039,-2559],[-630,-1073]],[[272492,884067],[2983,348]],[[275475,884415],[1463,-1942]],[[276938,882473],[-2067,-1781]],[[274871,880692],[-3699,1453]],[[271172,882145],[-248,1304]],[[270924,883449],[-2853,1020]],[[268071,884469],[-650,-1693]],[[267421,882776],[-2513,-2986]],[[264908,879790],[-2396,-1008]],[[262512,878782],[-858,3361],[-2896,-777]],[[258758,881366],[-950,574]],[[257808,881940],[2603,2752],[-340,2542]],[[260071,887234],[1146,6745]],[[261217,893979],[1131,1269]],[[262348,895248],[1764,-3895]],[[264792,893213],[-1282,1457]],[[263510,894670],[549,1112]],[[264059,895782],[733,-2569]],[[267428,894527],[-1090,-148]],[[266338,894379],[-802,2127],[1892,-1979]],[[295495,906299],[-2644,265],[-370,1413],[3532,-575]],[[296013,907402],[-518,-1103]],[[259456,906015],[-1011,2159]],[[258445,908174],[1496,493]],[[259941,908667],[-485,-2652]],[[291239,908966],[74,-4127]],[[291313,904839],[-1813,-1504],[-3403,-98]],[[286097,903237],[-836,2600],[1571,3113]],[[286832,908950],[2957,540],[1450,-524]],[[291997,909646],[-1443,1047],[1158,724]],[[291712,911417],[285,-1771]],[[279970,912589],[-542,459]],[[279428,913048],[2248,2652]],[[281676,915700],[1021,-396]],[[282697,915304],[-2727,-2715]],[[286124,914356],[-898,1615]],[[285226,915971],[1591,-74],[-693,-1541]],[[277839,916524],[-2223,991]],[[275616,917515],[3743,655],[-1520,-1646]],[[232499,915545],[1985,-3018]],[[234484,912527],[-2266,-2158]],[[232218,910369],[-2974,431]],[[229244,910800],[-5469,2217]],[[223775,913017],[-41,1263],[2777,1206]],[[226511,915486],[521,2488],[1327,635]],[[228359,918609],[975,-1297],[3165,-1767]],[[164773,916863],[2302,-1330],[2734,-506]],[[169809,915027],[2148,-1269],[5655,-1220],[1190,804]],[[178802,913342],[3380,-1855],[1250,-1543]],[[183432,909944],[-2225,-764],[-1858,-2180]],[[179349,907000],[4868,-1198]],[[184217,905802],[3463,-90]],[[187680,905712],[4529,875],[1952,947]],[[194161,907534],[1310,-1538]],[[195471,905996],[2882,-840]],[[198353,905156],[1843,-2750]],[[200196,902406],[-1574,-204]],[[198622,902202],[2183,-2087],[232,1559]],[[201037,901674],[1306,-719]],[[202343,900955],[-2266,5037]],[[200077,905992],[587,1779],[3713,997]],[[204377,908768],[1785,1931]],[[206162,910699],[-2297,-1002]],[[203865,909697],[-2809,-156]],[[201056,909541],[-318,-933]],[[200738,908608],[-2645,614]],[[198093,909222],[1036,1975]],[[199129,911197],[5969,1833],[1736,-1193]],[[206834,911837],[309,-1542]],[[207143,910295],[3430,-2530]],[[210573,907765],[1999,496]],[[212572,908261],[2172,-1799],[3159,-701]],[[217903,905761],[3052,868]],[[220955,906629],[3637,-687],[2041,495]],[[226633,906437],[2659,-1127]],[[229292,905310],[690,1410]],[[229982,906720],[-2512,285],[-1499,2729]],[[225971,909734],[3444,787],[1904,-2578]],[[231319,907943],[2097,1113]],[[233416,909056],[-1115,-4119]],[[232301,904937],[639,-1671]],[[232940,903266],[1171,265]],[[234111,903531],[1016,-1990]],[[235127,901541],[265,1670]],[[235392,903211],[-1089,2813]],[[234831,907706],[1666,120],[3381,3504]],[[239878,911330],[-2552,1651]],[[237326,912981],[1336,1328]],[[238662,914309],[-525,1891],[-4943,2228]],[[233194,918428],[-1377,2939],[1853,1314]],[[233670,922681],[-1862,1538],[398,2755]],[[232206,926974],[2338,374],[-856,1401]],[[233688,928749],[1864,1958]],[[235552,930707],[1789,446]],[[237341,931153],[4468,-4247]],[[241809,926906],[-91,-2428]],[[241718,924478],[2771,-3358]],[[244489,921120],[20,-1462]],[[244509,919658],[-2531,-2195]],[[241978,917463],[2711,-812],[3769,-158]],[[248458,916493],[-1896,-1297]],[[246562,915196],[2137,-2499]],[[248699,912697],[-293,-2305]],[[248406,910392],[1109,-1212],[1411,4410]],[[250926,913590],[1694,1490],[2821,-2691],[413,-3339],[-1262,237],[419,-3095]],[[255011,906192],[2581,-3447]],[[257592,902745],[2028,1968]],[[259620,904713],[1623,3296]],[[261243,908009],[1207,4132]],[[262450,912141],[1821,1801],[-1457,936]],[[262814,914878],[-78,3659],[3259,-87]],[[265995,918450],[1601,-800]],[[267596,917650],[3586,-343],[-1057,-874]],[[270125,916433],[3962,-2218]],[[274087,914215],[-1748,-1400]],[[272339,912815],[1879,-1341]],[[274218,911474],[-3531,-1249]],[[270687,910225],[1600,-3463]],[[272287,906762],[1962,-2382]],[[274249,904380],[-548,-2310],[-3261,-2858]],[[270440,899212],[-2449,-1296]],[[267991,897916],[-1103,1836],[-2571,2073]],[[264317,901825],[2833,-4376]],[[267150,897449],[-1813,-656]],[[265337,896793],[-2677,2121]],[[262660,898914],[-3309,-35]],[[259351,898879],[1640,-3015],[-3467,-3955],[-1885,-36],[-4943,3479]],[[250696,895352],[-3501,175],[3393,-1356],[2262,-2301]],[[252850,891870],[5407,-890]],[[258257,890980],[-703,-2203]],[[257554,888777],[-2293,-3809]],[[255261,884968],[-1977,-1132]],[[253284,883836],[-3751,-80]],[[249533,883756],[-215,-1995],[-1573,-362],[-2862,691],[3042,-2050]],[[247925,880040],[-86,-2251],[-1864,-992]],[[245975,876797],[-2534,90],[981,-1652]],[[244422,875235],[-1510,36]],[[242912,875271],[66,-2240]],[[242978,873031],[-2928,-1341]],[[240050,871690],[750,-1036]],[[240800,870654],[-2080,-2662]],[[238720,867992],[-21,-1061],[-1607,-4281]],[[237092,862650],[-348,-2456]],[[194439,937095],[3463,-2553],[1513,-4739]],[[199415,929803],[2511,850],[-1081,1509]],[[200845,932162],[-1507,5667]],[[199338,937829],[581,1439]],[[199919,939268],[2995,-430],[3162,-1573]],[[206076,937265],[4064,-9341]],[[210140,927924],[-611,-1954]],[[209529,925970],[1711,-2023]],[[211240,923947],[2511,-638],[4131,-3081],[1444,-143]],[[219326,920085],[299,-2344],[-3609,753]],[[216016,918494],[-1075,-1723]],[[214941,916771],[-2050,794]],[[212891,917565],[747,-2805]],[[213638,914760],[2608,1633],[818,-2747],[-2884,-1187]],[[214180,912459],[-6141,574]],[[208039,913033],[240,953]],[[208279,913986],[-3115,478]],[[205164,914464],[-1441,1644],[-2167,-2591]],[[201556,913517],[-4184,-1435],[-6569,-1291]],[[190803,910791],[-5047,-284]],[[185756,910507],[-1359,2040]],[[184397,912547],[-214,2113]],[[184183,914660],[-4069,413]],[[180114,915073],[-3763,947]],[[176351,916020],[-953,1333]],[[202996,940045],[854,1278]],[[203850,941323],[3059,415],[2400,-896]],[[209309,940842],[183,-1543]],[[209492,939299],[-1963,-2571],[-4533,3317]],[[279063,941080],[3474,67]],[[282537,941147],[3000,-985]],[[285537,940162],[2547,-2480]],[[288084,937682],[-308,-1543],[-3987,452]],[[283789,936591],[-4624,-835]],[[279165,935756],[-1897,2777]],[[277268,938533],[-1780,924]],[[275488,939457],[171,2235],[3404,-612]],[[259474,925417],[4151,836]],[[263625,926253],[624,-889]],[[264249,925364],[584,3462]],[[264833,928826],[-3477,2372]],[[261356,931198],[1405,1353],[2929,-962],[-2749,2185]],[[262941,933774],[-843,2091],[1740,3325],[3434,482]],[[267272,939672],[3118,1852],[3481,-563],[3144,-5266],[-2652,-2571]],[[274363,933124],[1030,-2372],[2268,2861]],[[277661,933613],[3730,-255],[2703,-1014]],[[284094,932344],[-2036,2147],[4349,1056]],[[286407,935547],[4442,-1421]],[[290849,934126],[1086,-2253]],[[291935,931873],[1695,-297]],[[293630,931576],[-308,-2239]],[[293322,929337],[4172,31],[4572,-1872]],[[302066,927496],[-663,-1520],[1951,-12]],[[303354,925964],[381,-1376]],[[303735,924588],[-1724,-2195]],[[302011,922393],[3684,2042]],[[305695,924435],[4421,-1908]],[[310116,922527],[-1557,-1872]],[[308559,920655],[485,-1575],[1732,2213]],[[310776,921293],[2102,-1660]],[[312878,919633],[395,-1800]],[[313273,917833],[-2479,139]],[[310794,917972],[-1108,-1048]],[[309686,916924],[4839,-1425]],[[314525,915499],[-89,-1090]],[[314436,914409],[-3154,565]],[[311282,914974],[518,-1241]],[[311800,913733],[-795,-2890]],[[311005,910843],[3678,-624]],[[314683,910219],[-325,-1362]],[[314358,908857],[1719,384],[-560,-2229],[3991,825]],[[319508,907837],[2280,-2491]],[[321788,905346],[889,-2126]],[[322677,903220],[2211,-173],[-1837,-2444]],[[323051,900603],[4814,1166],[1858,-2196]],[[329723,899573],[-1564,-1989]],[[328159,897584],[-1918,557]],[[326241,898141],[1560,-2201]],[[327801,895940],[-1663,-5]],[[326138,895935],[-191,-2337]],[[325947,893598],[-1885,-137],[-747,-4081]],[[323315,889380],[-803,1074]],[[322512,890454],[-1832,43]],[[320680,890497],[-2351,3836]],[[318329,894333],[1103,1858]],[[319432,896191],[-2282,-478]],[[317150,895713],[-2671,3310]],[[314479,899023],[-1455,-1493],[-1548,1105]],[[311476,898635],[1904,-2700]],[[313380,895935],[-2982,-568],[3163,-2952],[-578,-496]],[[312983,891919],[1832,-2268],[2022,-521]],[[316837,889130],[2400,-2661]],[[319237,886469],[-265,-2421]],[[318972,884048],[1365,0]],[[320337,884048],[456,-4526],[-1882,2963]],[[318911,882485],[396,-3137]],[[319307,879348],[1016,-1969]],[[320323,877379],[-1331,179]],[[318992,877558],[313,-1697]],[[319305,875861],[-3261,2731],[-529,-473]],[[315515,878119],[-2126,1645]],[[313389,879764],[-1982,2540]],[[311407,882304],[474,-1842]],[[311881,880462],[-2142,1793],[-1159,-131]],[[308580,882124],[2138,-3145],[1293,-467]],[[312011,878512],[4710,-5241]],[[316721,873271],[-768,-2018]],[[315953,871253],[-3287,1676]],[[312666,872929],[-2607,498],[-1955,1007]],[[308104,874434],[-1285,2011],[-1920,111]],[[304899,876556],[-2826,1653],[-1998,2296],[1436,487],[-2131,1165]],[[299380,882157],[-3390,4234]],[[295990,886391],[-4091,2183]],[[291899,888574],[767,-1391],[-5788,-1869],[-2965,743]],[[283913,886057],[-1065,1485]],[[282848,887542],[219,1905]],[[283067,889447],[2041,1524]],[[285108,890971],[95,1520]],[[285203,892491],[4162,-1340],[333,525]],[[289698,891676],[5994,1005]],[[295692,892681],[-542,1668],[-1911,2205],[3203,3175],[2947,3433]],[[299389,903162],[-3020,6597]],[[296369,909759],[-937,-434]],[[295432,909325],[-2788,2445],[-574,2124],[-4302,-2212]],[[287768,911682],[-427,1878]],[[287341,913560],[1676,127]],[[289017,913687],[463,1704]],[[289480,915391],[-1725,727]],[[287755,916118],[-292,1438]],[[287463,917556],[-2996,958]],[[284467,918514],[-697,2378]],[[283770,920892],[-1223,-106]],[[282547,920786],[-2176,2217],[-781,-1369],[1272,-2338],[-2018,-491]],[[278844,918805],[-5399,1283]],[[273445,920088],[-1608,-1600]],[[271837,918488],[-2646,964]],[[269191,919452],[-2133,-244]],[[267058,919208],[-6842,1081]],[[260216,920289],[-839,1516]],[[259377,921805],[-3546,-884]],[[255831,920921],[-2632,1606]],[[253199,922527],[-1688,3192]],[[251511,925719],[4475,-695]],[[255986,925024],[1957,398]],[[257943,925422],[-2033,1166],[-3353,471]],[[252557,927059],[-2129,1211]],[[250428,928270],[-498,2703]],[[249930,930973],[453,2745]],[[250383,933718],[1662,3892],[4288,3875]],[[256333,941485],[2641,658]],[[258974,942143],[4608,-153]],[[263582,941990],[-4219,-5554]],[[259363,936436],[1647,-6515]],[[261010,929921],[2814,-2475]],[[263824,927446],[-4350,-2029]],[[224561,941535],[4378,925]],[[228939,942460],[1612,-1311],[-753,-1655]],[[229798,939494],[-3234,-2290]],[[226564,937204],[2670,-48]],[[229234,937156],[880,-2070]],[[230114,935086],[1713,331]],[[231827,935417],[-705,-2280]],[[231122,933137],[507,-2844]],[[231629,930293],[-2691,-1209]],[[228938,929084],[-2435,850]],[[226503,929934],[723,-1969]],[[227226,927965],[-2690,-437],[-3965,4651]],[[220571,932179],[-3137,964],[-2750,2773]],[[214684,935916],[2198,1623]],[[216882,937539],[1588,-1840]],[[218470,935699],[2405,158]],[[220875,935857],[1078,1127]],[[221953,936984],[-951,1726]],[[221002,938710],[-2810,1045]],[[218192,939755],[1488,2218]],[[219680,941973],[2536,833]],[[223930,942411],[4686,1359],[-1589,-1423],[-3097,64]],[[241192,944080],[2634,-1117]],[[243826,942963],[5189,-616]],[[249015,942347],[-4898,-6604],[-5815,19]],[[238302,935762],[1822,-1989]],[[240124,933773],[-1340,-2325]],[[238784,931448],[-3209,-8]],[[235575,931440],[-985,4468]],[[234590,935908],[-237,5414]],[[234353,941322],[2598,-189]],[[236951,941133],[-951,2135]],[[236000,943268],[5192,812]],[[210778,949266],[-2132,660]],[[208646,949926],[1503,1671]],[[210149,951597],[1956,-1581]],[[212105,950016],[-1327,-750]],[[240159,949216],[-12,-1995]],[[240147,947221],[-3196,-291]],[[236951,946930],[-5173,2063],[2469,3190]],[[234247,952183],[3455,383]],[[237702,952566],[2457,-3350]],[[216034,955063],[-3020,-1485],[-2878,2478]],[[210136,956056],[4908,588]],[[215044,956644],[990,-1581]],[[211047,958429],[2699,-788]],[[213746,957641],[-3387,-733]],[[210359,956908],[688,1521]],[[228608,957739],[1014,-6202]],[[229622,951537],[-940,-1733]],[[228682,949804],[-2484,-698]],[[226198,949106],[-4627,-9]],[[221571,949097],[-1380,2007]],[[220191,951104],[3167,1830]],[[223358,952934],[-8195,-840]],[[215163,952094],[1662,2193]],[[216825,954287],[235,3289]],[[217060,957576],[5106,-2959]],[[222166,954617],[-2419,3175]],[[219747,957792],[6056,1294]],[[225803,959086],[2805,-1347]],[[194437,952245],[2939,947],[-2365,972]],[[195011,954164],[1016,1458]],[[196027,955622],[-1592,2195],[1847,1661]],[[196282,959478],[2420,-133],[-126,-1769]],[[198576,957576],[1834,-2259]],[[200410,955317],[-468,-1498]],[[199942,953819],[3136,-133]],[[203078,953686],[1376,1646]],[[204454,955332],[2543,-1863]],[[206997,953469],[-1060,-3283]],[[205937,950186],[-3585,-1567],[-4661,816]],[[197691,949435],[-3254,-1267]],[[238068,960381],[3611,-1730]],[[241679,958651],[4696,357],[5273,-2912]],[[251648,956096],[-5202,-173]],[[246446,955923],[5762,-2358],[-1225,-1167],[2026,-658]],[[253009,951740],[4609,970],[3302,-684]],[[260920,952026],[5935,1877]],[[266855,953903],[4940,71]],[[271795,953974],[5088,-1196]],[[276883,952778],[1910,-2546]],[[278793,950232],[-2009,-876],[2656,-793]],[[279440,948563],[-2434,-1991]],[[277006,946572],[-4253,-622]],[[272753,945950],[-9035,772]],[[263718,946722],[-2484,-641],[-6854,-27]],[[254380,946054],[231,1722]],[[254611,947776],[-4179,-1400],[-5881,1450]],[[244551,947826],[-1293,3277]],[[243258,951103],[995,1844],[-2842,4126],[-6062,-532]],[[235349,956541],[-4365,2738]],[[230984,959279],[243,1454]],[[231227,960733],[3111,545]],[[234338,961278],[3730,-897]],[[250463,962485],[-3651,710],[-5,1307]],[[246807,964502],[2715,-79]],[[249522,964423],[941,-1938]],[[209605,962901],[-1869,-923],[-2364,3220],[4233,-2297]],[[234765,965592],[5282,-126]],[[240047,965466],[176,-1756],[-6854,57]],[[233369,963767],[1396,1825]],[[232764,969972],[3660,-1103],[-556,-2089],[-5284,-1106]],[[230584,965674],[-3516,3693]],[[227068,969367],[120,2224]],[[227188,971591],[5576,-1619]],[[215066,972034],[2424,1183],[5817,-2939],[-394,-1659]],[[222913,968619],[1625,-2642]],[[224538,965977],[-3944,205]],[[220594,966182],[-1356,1790],[-4603,1051]],[[214635,969023],[-4425,-603],[-1865,1476]],[[208345,969896],[3420,6]],[[211765,969902],[-1076,2786]],[[210689,972688],[-1849,-1082]],[[208840,971606],[-1995,1336],[411,1724]],[[207256,974666],[5449,-48],[2361,-2584]],[[225579,978561],[-137,-1446],[-3477,1076]],[[221965,978191],[1003,1336],[2611,-966]],[[244762,985385],[3451,-3194]],[[248213,982191],[8245,-1313]],[[256458,980878],[-687,-1627]],[[255771,979251],[2624,-1206],[-882,-1859]],[[257513,976186],[4576,185]],[[262089,976371],[995,-2388]],[[263084,973983],[-6467,-3153]],[[256617,970830],[-3122,-546]],[[253495,970284],[-137,-2320]],[[253358,967964],[-3461,2456]],[[249897,970420],[1472,-2391]],[[251369,968029],[-6645,199]],[[244724,968228],[-6288,4486]],[[238436,972714],[7831,2173]],[[246267,974887],[-4106,527]],[[242161,975414],[-6338,-948]],[[235823,974466],[-4639,5012]],[[231184,979478],[5911,-516],[-4785,1448]],[[232310,980410],[2276,435]],[[234586,980845],[-1622,1924],[6125,-783]],[[239089,981986],[-4409,1652]],[[234680,983638],[680,965],[7938,1643]],[[243298,986246],[1464,-861]],[[306975,996546],[8048,-431]],[[315023,996115],[-2237,-1635]],[[312786,994480],[6923,1379],[5054,-1988]],[[324763,993871],[4467,-581]],[[329230,993290],[-1943,-2511]],[[327287,990779],[-6660,-1835],[-5699,-694]],[[314928,988250],[-4700,-2105]],[[317401,987526],[699,-1242]],[[318100,986284],[-8741,-3591]],[[309359,982693],[-7659,-5433],[-5791,-30],[18,-1548]],[[295927,975682],[-4981,-439]],[[290946,975243],[-4554,541]],[[286392,975784],[6574,-2724],[-11249,133]],[[281717,973193],[1942,-786]],[[283659,972407],[4519,382]],[[288178,972789],[5063,-1675]],[[293241,971114],[-1237,-1062]],[[292004,970052],[-4271,-198],[3396,-1088]],[[291129,968766],[-1868,-1884],[-5963,-377]],[[283298,966505],[-177,-2530]],[[283121,963975],[-2203,-1105]],[[280918,962870],[-6244,-373]],[[274674,962497],[4934,-659]],[[279608,961838],[334,-1317]],[[279942,960521],[2589,247]],[[282531,960768],[12,-2409],[-6683,-2338]],[[275860,956021],[-1334,1992]],[[274526,958013],[-3776,1247],[824,-1525]],[[271574,957735],[-4590,-75]],[[266984,957660],[-3488,-880]],[[263496,956780],[-2707,772]],[[260789,957552],[-6334,-177]],[[254455,957375],[-3261,515]],[[251194,957890],[196,1984],[3059,1641]],[[254449,961515],[4294,418],[-3452,3228]],[[255291,965161],[2992,1025]],[[258283,966186],[3971,-2555]],[[262254,963631],[2361,-592]],[[264615,963039],[2664,1016]],[[267279,964055],[-4194,157]],[[263085,964212],[-717,2184]],[[262368,966396],[1453,2279]],[[263821,968675],[-3315,-1370]],[[260506,967305],[827,1550]],[[261333,968855],[-4533,-984],[2067,3540]],[[258867,971411],[5011,818]],[[263878,972229],[4812,-841]],[[268690,971388],[2313,790]],[[271003,972178],[-3159,889],[-4205,3308],[-3697,1381]],[[259942,977756],[316,2809]],[[260258,980565],[7176,-537],[5189,-2999]],[[272623,977029],[2348,-174],[-5419,3465]],[[269552,980320],[8084,1485]],[[277636,981805],[8891,2071]],[[286527,983876],[-4725,256],[735,1458],[-7557,-3037]],[[274980,982553],[-8526,-584],[-9037,672]],[[257417,982641],[-4421,805]],[[252996,983446],[1412,1150]],[[254408,984596],[-4262,1024],[4079,2323]],[[254225,987943],[-8122,80]],[[246103,988023],[2535,1771]],[[248638,989794],[7920,1232]],[[256558,991026],[7206,-606]],[[263764,990420],[-4267,1212],[4678,1552]],[[264175,993184],[15200,-3524]],[[279375,989660],[-8407,3393]],[[270968,993053],[4003,2085],[6282,-591]],[[281253,994547],[-3906,1373]],[[277347,995920],[20398,1007],[9230,-381]],[[269941,777103],[157,-702],[2294,718]],[[272392,777119],[160,-2629]],[[272552,774490],[-3724,2172]],[[268828,776662],[1113,441]],[[279110,806663],[-539,898]],[[278571,807561],[536,2530]],[[279107,810091],[3,-3428]],[[279112,806382],[18,-19952],[412,-3135],[1622,-3722],[3051,-1226],[1248,-1677],[1138,-142],[1139,-2271],[1415,-451],[2888,1315],[1299,-440],[156,-2093]],[[293498,772588],[-1023,-1248]],[[292475,771340],[-1307,-619]],[[291168,770721],[-1718,-2421]],[[289450,768300],[-720,-735]],[[288730,767565],[-2939,-1148],[656,-1362]],[[286447,765055],[-914,-398]],[[285533,764657],[-1043,963]],[[284490,765620],[-4103,-1205]],[[280387,764415],[-1412,-1521],[-570,-1532]],[[278405,761362],[1213,-722]],[[279618,760640],[767,504]],[[280385,761144],[-13,-1048]],[[280372,760096],[25,-1444]],[[280397,758652],[-3232,-520]],[[277165,758132],[-641,-954],[-2648,99],[-1284,-2203],[-2162,-1310],[-1286,200],[88,1212]],[[269232,755176],[1352,232]],[[270584,755408],[-154,1442]],[[270430,756850],[633,2721]],[[271063,759571],[1757,1834],[213,4762]],[[273033,766167],[1195,3039],[-1165,3598],[1113,-13]],[[274176,772791],[103,-1306]],[[274279,771485],[1011,-1465]],[[275290,770020],[1860,-1609]],[[277150,768411],[327,1762]],[[277477,770173],[1085,-270],[-1023,2062]],[[277539,771965],[124,1381]],[[277663,773346],[-857,337]],[[276806,773683],[-1300,3268],[-2164,93]],[[273342,777044],[-52,905]],[[273290,777949],[-3917,366],[-2962,1065]],[[266411,779380],[-204,1195]],[[266207,780575],[-931,-470]],[[265276,780105],[321,2503]],[[265597,782608],[-1074,776]],[[264523,783384],[493,1680],[-1167,1886],[443,1844]],[[264292,788794],[-2563,120],[-908,1422],[-811,3086]],[[260010,793422],[-2298,265]],[[257712,793687],[-2889,1172]],[[254823,794859],[85,-2231]],[[254908,792628],[-749,1398]],[[254159,794026],[-755,-1481]],[[253404,792545],[-1184,-783]],[[252220,791762],[-1047,-2662]],[[251173,789100],[-3508,1179]],[[247665,790279],[-1884,-843]],[[245781,789436],[-4105,3279]],[[241676,792715],[-1975,-511]],[[239701,792204],[-2537,1286]],[[237164,793490],[-650,3328],[-835,387]],[[252921,841529],[2426,-2274]],[[255347,839255],[1427,-2435]],[[256774,836820],[5235,-2697]],[[262009,834123],[1710,-1869]],[[263719,832254],[3196,172]],[[266915,832426],[3703,-983]],[[270618,831443],[994,-1986],[-551,-2711],[768,-3189]],[[271829,823557],[-331,-5075]],[[271498,818482],[1914,-3518]],[[273412,814964],[61,-774]],[[273473,814190],[2477,-2833]],[[275950,811357],[499,-2673],[1041,-144]],[[277490,808540],[1622,-2158]],[[323107,780571],[1533,-828]],[[324640,779743],[2683,385]],[[327323,780128],[388,-389],[-2373,-2489]],[[325338,777250],[-485,1590]],[[324853,778840],[-622,-690]],[[324231,778150],[-1339,1447],[-978,164]],[[321914,779761],[-770,1278]],[[321144,781039],[1096,2492]],[[322240,783531],[-262,-1695]],[[321978,781836],[1129,-1265]],[[295648,774097],[-1094,-164]],[[294554,773933],[1345,1559],[-251,-1395]],[[308184,784912],[-526,997]],[[307658,785909],[-2124,-4467]],[[305534,781442],[-802,-4757],[-1671,-3813]],[[303061,772872],[-518,187]],[[302543,773059],[-675,-23]],[[301868,773036],[-528,-1674]],[[301340,771362],[-5096,-12]],[[296244,771350],[-3769,-10]],[[292475,771340],[3197,2496]],[[295672,773836],[1107,3465]],[[296779,777301],[3496,3684],[1777,737]],[[302052,781722],[2060,1637]],[[304112,783359],[4257,7361]],[[308369,790720],[3962,3441]],[[312331,794161],[3840,2116],[1819,315]],[[317990,796592],[1909,-441]],[[319899,796151],[1596,-1599]],[[321495,794552],[-242,-2954]],[[321253,791598],[-2530,-2381],[-1853,993]],[[316870,790210],[-2160,-986]],[[327168,795484],[-3740,1896]],[[323428,797380],[-886,1532]],[[322542,798912],[-1543,1007],[858,675]],[[321857,800594],[3536,-1400],[2892,-2499]],[[328285,796695],[45,-1124]],[[341388,809491],[-3917,-879]],[[337471,808612],[-1820,-3052]],[[335651,805560],[-2541,-3112]],[[333110,802448],[-3360,-312]],[[328542,801556],[-541,763]],[[328001,802319],[-2211,408]],[[325790,802727],[-5979,-155]],[[319811,802572],[-1113,263]],[[318698,802835],[-3408,-641]],[[315290,802194],[-1238,-1291],[-1197,-3823]],[[312855,797080],[-1901,-543],[-2424,-2535]],[[308530,794002],[-2069,-3731]],[[306461,790271],[-2297,918],[2016,-1517]],[[306180,789672],[-362,-1575]],[[305818,788097],[-2223,-4104],[-1561,-2036]],[[302034,781957],[-1700,-646]],[[300334,781311],[-3059,-2827]],[[297275,778484],[-1377,-2793]],[[295898,775691],[-1559,-1400]],[[294339,774291],[178,-929]],[[294517,773362],[-1019,-774]],[[279112,806382],[-2,281]],[[279107,810091],[1336,-479]],[[280443,809612],[381,-1561]],[[280824,808051],[477,1760],[-684,1399]],[[280617,811210],[1350,3072]],[[281967,814282],[-644,2225]],[[281323,816507],[-1082,6455]],[[280241,822962],[469,729]],[[280710,823691],[-2003,5078],[2101,1090],[2827,2104],[1573,1889]],[[285208,833852],[1874,3270]],[[287082,837122],[363,3553]],[[287445,840675],[-148,2809]],[[287297,843484],[-1622,4963]],[[285675,848447],[-3773,3931]],[[281902,852378],[157,1131]],[[282059,853509],[1939,3002]],[[283998,856511],[96,1753]],[[284094,858264],[1096,715]],[[285190,858979],[55,1456]],[[285245,860435],[-1331,3540]],[[283914,863975],[522,1098]],[[284436,865073],[-1607,-36]],[[282829,865037],[1853,4366]],[[284682,869403],[-1203,1219]],[[283479,870622],[-335,3516]],[[283144,874138],[1932,1287]],[[285076,875425],[4321,-1521]],[[289397,873904],[3253,-620]],[[292650,873284],[2822,1440]],[[295472,874724],[3900,-3689]],[[299372,871035],[2231,-3984],[3177,-536],[510,-1116],[1732,775],[-927,-6316]],[[306095,859858],[628,-1598],[-284,-1976]],[[307222,856261],[-366,-2776]],[[306856,853485],[2936,-271]],[[309792,853214],[669,-2514]],[[310461,850700],[1845,-1100]],[[312306,849600],[2672,1987]],[[314978,851587],[2782,3329]],[[317760,854916],[556,4055]],[[318316,858971],[1319,2706]],[[319635,861677],[880,320]],[[218424,794974],[-7407,0]],[[211017,794974],[-9175,0],[-7396,0]],[[113732,860194],[-65,2027],[-2483,-952]],[[111184,861269],[-2857,694]],[[108327,861963],[0,55397]],[[108327,917360],[5057,-801],[2926,-2154]],[[116310,914405],[4680,-1522]],[[6513,810872],[1645,1335],[-238,-1097],[-1407,-238]],[[9463,811999],[432,-667]],[[9895,811332],[-1456,-891]],[[8439,810441],[1024,1558]],[[13068,812920],[2748,1149]],[[15816,814069],[-1032,-1074],[-1716,-75]],[[33432,820758],[-3061,-3029]],[[30371,817729],[1969,3695]],[[32340,821424],[1032,595]],[[33372,822019],[60,-1261]],[[34659,820350],[-795,281],[1868,1201]],[[35732,821832],[440,2586]],[[36172,824418],[2075,-180],[-1499,-2705],[-2089,-1183]],[[45900,830448],[327,-1453]],[[46227,828995],[-4071,-1875]],[[42156,827120],[-178,1117],[994,1619]],[[42972,829856],[1839,938]],[[44811,830794],[1089,-346]],[[129708,833783],[-959,-1626]],[[128749,832157],[44,1600],[915,26]],[[136169,833460],[-581,-1676]],[[135588,831784],[-721,1199]],[[134867,832983],[-875,-1440],[-231,1485]],[[133761,833028],[614,2461]],[[135363,836221],[806,-2761]],[[128983,838496],[1009,-115]],[[129992,838381],[2860,-4972]],[[132852,833409],[-1162,-96]],[[131690,833313],[1708,-1515],[-438,-2939]],[[132960,828859],[-1800,1990]],[[131160,830849],[-892,1846]],[[130268,832695],[193,1360],[-1795,1158]],[[128666,835213],[317,3283]],[[132963,836150],[-1464,799]],[[131499,836949],[779,2491]],[[132278,839440],[685,-3290]],[[127916,837243],[-665,-301]],[[127251,836942],[-512,4513]],[[126739,841455],[1067,36],[374,-1556],[-264,-2692]],[[129538,842431],[1145,-729]],[[130683,841702],[-719,-2463]],[[129964,839239],[-1084,-3]],[[128880,839236],[-1046,3232]],[[127834,842468],[1704,-37]],[[125084,844493],[969,-3752]],[[126053,840741],[-93,-2907]],[[125960,837834],[-614,2624],[-1265,897],[363,1217]],[[124444,842572],[-838,1283],[-863,-1389]],[[122743,842466],[69,1825]],[[122812,844291],[1226,1279],[1046,-1077]],[[75283,847292],[1304,11]],[[76587,847303],[590,-1474]],[[77177,845829],[-2940,-2078]],[[74237,843751],[-1341,-2179],[-1352,1686]],[[71544,843258],[-47,-1370]],[[71497,841888],[-1237,2510]],[[70260,844398],[475,1327]],[[70735,845725],[1404,422]],[[72139,846147],[349,1121]],[[72488,847268],[1156,-527],[910,1427]],[[74554,848168],[729,-876]],[[123296,848287],[1697,351]],[[124993,848638],[197,-3377]],[[125190,845261],[-1573,1074],[-541,-1436],[-2434,3271],[684,1462],[1644,151],[326,-1496]],[[125887,849293],[1223,-104],[-74,-1538]],[[127036,847651],[948,-3245]],[[127984,844406],[-1851,-1451],[292,2312],[-1240,5017],[702,-991]],[[76620,850469],[853,-1179],[-1866,-861]],[[75607,848429],[-1668,423],[1502,1950]],[[75441,850802],[1179,-333]],[[89622,859078],[1542,3229]],[[91164,862307],[539,-616]],[[91703,861691],[-2081,-2613]],[[38512,862457],[1126,-411],[385,-2376]],[[40023,859670],[-1431,-816],[-2868,1381]],[[35724,860235],[-825,1173]],[[34899,861408],[1666,62]],[[36565,861470],[1947,987]],[[23714,881749],[2868,349]],[[26582,882098],[2784,-2077],[1977,-223]],[[31343,879798],[-377,-826]],[[30966,878972],[-2572,-459]],[[28394,878513],[-2080,1691]],[[26314,880204],[-3512,269]],[[22802,880473],[912,1276]],[[138819,835824],[-33,-3497]],[[138786,832327],[-1496,-3131],[-762,226],[-550,2074]],[[135978,831496],[591,1033]],[[136569,832529],[-848,3942]],[[135721,836471],[-1788,-716]],[[133933,835755],[-554,-2023]],[[133379,833732],[-667,1102],[1054,2601]],[[133766,837435],[-2970,5257]],[[129261,843431],[-245,3098]],[[129016,846529],[-1819,3187],[-1264,899]],[[125933,850615],[-1300,2714],[-645,3415],[-384,-1286],[1258,-5305]],[[124862,850153],[-2289,517],[-759,2339]],[[121814,853009],[-2340,1455]],[[119474,854464],[2577,-3447],[-1447,-1230]],[[120604,849787],[-2671,1992]],[[117933,851779],[-2246,2998]],[[115687,854777],[-4019,2719]],[[111668,857496],[1302,1960],[-524,830],[-1937,-1721]],[[110509,858565],[-1741,131]],[[108768,858696],[-2296,1310]],[[106472,860006],[-3544,753]],[[102928,860759],[-3338,-478],[-2094,1888]],[[97496,862169],[584,1979]],[[98080,864148],[-1548,-1711],[-1808,580]],[[94724,863017],[-2054,2483]],[[92670,865500],[155,1366]],[[92825,866866],[-2245,-1243],[-1707,299],[705,1484]],[[89578,867406],[-2239,-2466],[1649,-1883]],[[88988,863057],[-1297,-2937]],[[87691,860120],[-2679,691]],[[85012,860811],[-563,-1987]],[[84449,858824],[-2732,-1219]],[[81717,857605],[-980,-1869]],[[80737,855736],[-2234,-359],[-309,1290],[1251,652],[-1261,1574]],[[78184,858893],[1116,2491]],[[79300,861384],[265,3084],[2541,1780],[2359,-176]],[[84465,866072],[-980,1780],[-1853,41],[-3116,-2313]],[[78516,865580],[-46,-924],[-2510,-3060]],[[75960,861596],[-292,-1880]],[[75668,859716],[-1255,-464]],[[74413,859252],[-2436,-2840]],[[71977,856412],[-250,-1231]],[[71727,855181],[2343,-1763]],[[74070,853418],[-1904,-2162]],[[72166,851256],[-630,-1976]],[[71536,849280],[-2112,-851],[-2141,-2652]],[[67283,845777],[-1945,-1424]],[[65338,844353],[-420,-1884]],[[64918,842469],[-3444,-2160],[-1857,-1836]],[[59617,838473],[-700,-2064]],[[58917,836409],[-2649,-848]],[[56268,835561],[-2100,-1816],[-2726,-826]],[[51442,832919],[60,1370]],[[51502,834289],[-1708,-2902]],[[49794,831387],[-1300,613],[-368,-1458],[-1835,-933]],[[46291,829609],[156,1675]],[[46447,831284],[1035,437]],[[47482,831721],[2081,3103]],[[49563,834824],[2616,1788]],[[52179,836612],[2565,-1280]],[[54744,835332],[-655,948],[627,2067]],[[54716,838347],[3644,3235]],[[58360,841582],[3480,4076]],[[61840,845658],[594,5174]],[[62434,850832],[1060,2703]],[[63494,853535],[-2444,-1407]],[[61050,852128],[-2094,1554]],[[58956,853682],[-36,-2735]],[[58920,850947],[-817,171],[-1633,2615]],[[56470,853733],[-694,-540]],[[55776,853193],[-1229,1370]],[[54547,854563],[-2774,-2261]],[[51773,852302],[-2177,-150]],[[49596,852152],[1169,889],[-831,2901]],[[49934,855942],[541,1805],[-1646,4120]],[[48829,861867],[786,2379],[-1519,-2469],[317,-1654],[-3710,-1084],[-2099,2945],[-1921,1407],[1525,2078]],[[42208,865469],[2985,-1789]],[[45193,863680],[860,991]],[[46053,864671],[-4875,1234]],[[41178,865905],[833,718]],[[42011,866623],[-2265,1263],[-1324,2078]],[[38422,869964],[1541,1295]],[[39963,871259],[-262,1369]],[[39701,872628],[1425,2210],[1153,46]],[[42279,874884],[-183,1894]],[[42096,876778],[2050,2730],[2079,-1279]],[[46225,878229],[2048,1303],[940,1561]],[[49213,881093],[3286,170],[893,1546]],[[53392,882809],[-581,2562]],[[52811,885371],[-1186,1629]],[[51625,887000],[1341,313]],[[52966,887313],[-708,2043],[-1590,-638]],[[50668,888718],[-2910,-2619]],[[47758,886099],[-2518,1268]],[[45240,887367],[-3294,-756]],[[41946,886611],[-3455,724]],[[38491,887335],[-757,2036]],[[37734,889371],[-1426,1365],[2032,880]],[[38340,891616],[-3353,690],[-1899,1397]],[[33088,893703],[2816,1300]],[[35904,895003],[4514,3156]],[[40418,898159],[4783,1224]],[[45201,899383],[-850,-2375]],[[44351,897008],[938,-782],[5221,-177]],[[50510,896049],[1001,1348],[-1034,531]],[[50477,897928],[-2165,3101]],[[48312,901029],[1639,-653]],[[49951,900376],[300,-1330]],[[50251,899046],[3496,-1106]],[[53747,897940],[1079,1182]],[[54826,899122],[-1671,583],[-1484,-705]],[[51671,899000],[-1273,880]],[[50398,899880],[651,1653]],[[51049,901533],[-3832,284]],[[47217,901817],[-1997,997]],[[45220,902814],[-1125,2436],[-3501,2600]],[[40594,907850],[-3890,1860],[1128,388],[476,2726]],[[38308,912824],[5295,304],[3169,2674],[582,2193],[2440,2392],[2994,846]],[[52788,921233],[4671,3400],[3655,-197],[4246,3333],[6320,-3594]],[[71680,924175],[2673,778],[2778,-723]],[[77131,924230],[-661,-929]],[[76470,923301],[3461,-1391]],[[79931,921910],[5430,486],[4345,-1681],[5228,-339],[1740,-896]],[[96674,919480],[5497,637],[5029,-2743]],[[107200,917374],[1127,-14]],[[256972,684688],[-834,-509]],[[256138,684179],[-614,2384]],[[255524,686563],[-345,-1940]],[[255179,684623],[-734,25]],[[254445,684648],[-241,9003],[1116,18025],[-246,391]],[[255074,712067],[-48,154],[7130,-143]],[[262156,712078],[1175,-12113],[753,-4205],[-540,-2169],[324,-5206]],[[263868,688385],[-7184,-6],[478,-1950],[-190,-1741]],[[250820,718007],[-663,-1911],[-553,-3346],[-420,-671]],[[249184,712079],[-949,-3479],[-1208,-2604],[-383,-1769],[162,-3909]],[[246806,700318],[-8032,-23]],[[238774,700295],[-17,3214],[-1213,556]],[[237544,704065],[125,10302],[-498,6598]],[[237171,720965],[6963,0],[5415,0],[240,-1210],[-848,-1801],[1879,53]],[[197092,723926],[-3,-33609]],[[197089,690317],[-5539,-21]],[[191550,690296],[-3952,2631]],[[187598,692927],[-6587,4384]],[[181011,697311],[308,1227]],[[181319,698538],[688,750],[-632,1942],[544,4349],[1065,2267],[-681,1197],[-666,2977]],[[181637,712020],[55,2143],[-347,4338],[1868,573],[8,4872]],[[183221,723946],[5202,-7],[8669,-13]],[[154920,753549],[5885,-2],[5859,0]],[[166664,753547],[2,-17775],[5205,-7749],[5485,-8879],[4281,-7124]],[[181319,698538],[-6676,-1079],[-942,4515],[-1702,2528],[-917,129]],[[171082,704631],[-267,1621],[-1770,560]],[[169045,706812],[-1284,1813]],[[167761,708625],[-2431,318]],[[165330,708943],[-454,642]],[[164876,709585],[31,2941]],[[164907,712526],[-630,1712]],[[164277,714238],[-2826,5721],[-9,3601]],[[161442,723560],[-1429,1591]],[[160013,725151],[-331,3345],[1233,-1741]],[[160915,726755],[-875,2857],[1857,436]],[[161897,730048],[-2131,443],[-103,-1673]],[[159663,728818],[-1141,1356],[-525,2334]],[[157997,732508],[-1611,2714],[-511,5649],[-1220,2317]],[[154655,743188],[-132,1417]],[[154523,744605],[663,2836]],[[155186,747441],[179,2455]],[[155365,749896],[-445,3653]],[[216598,741701],[33,-17775]],[[216631,723926],[-2745,0]],[[213886,723926],[-5248,0],[-7347,0],[-4199,0]],[[197092,723926],[1,23698]],[[197093,747624],[8724,0],[5234,0]],[[211051,747624],[5546,-1],[1,-5922]],[[300553,753615],[-115,-4008]],[[300438,749607],[-3007,-298]],[[297431,749309],[-1960,-1738]],[[295471,747571],[416,6302]],[[295887,753873],[4666,-258]],[[290497,740602],[-463,-1035]],[[290034,739567],[543,-3247]],[[290577,736320],[985,-3774]],[[291562,732546],[-1860,-4],[-215,7508]],[[289487,740050],[1010,552]],[[273600,686784],[707,-5555]],[[274307,681229],[971,-4407]],[[275278,676822],[1044,-3340]],[[276322,673482],[-873,1608],[249,-2231]],[[275698,672859],[1451,-6955]],[[277149,665904],[514,-3783],[-327,-4089]],[[277336,658032],[-578,-3241]],[[276758,654791],[-1026,-1036]],[[275732,653755],[-1039,-109]],[[274693,653646],[-8,1358]],[[274685,655004],[-699,2747],[-974,902]],[[273012,658653],[-419,2677]],[[272593,661330],[-481,693]],[[272112,662023],[-76,2012]],[[272036,664035],[-620,-123]],[[271416,663912],[-960,3873]],[[270456,667785],[653,1841],[-497,729],[-226,-1422]],[[270386,668933],[-507,756]],[[269879,669689],[508,3791]],[[270387,673480],[25,2380]],[[270412,675860],[-1775,3344],[-1122,2808]],[[266544,683066],[-941,-1164]],[[265603,681902],[-2600,-1346]],[[263003,680556],[-97,1158]],[[262906,681714],[-1117,1726]],[[261789,683440],[-1941,1375]],[[259848,684815],[-2876,-127]],[[263868,688385],[343,-1663],[7345,-921],[494,-954],[351,2459],[1199,-522]],[[275354,694475],[-669,-895]],[[274685,693580],[-486,-3642],[-426,-380]],[[273773,689558],[-173,-2774]],[[262156,712078],[3609,-75]],[[265765,712003],[3360,78]],[[269125,712081],[-669,-1736],[1411,-1608],[718,-2485],[1621,-2930],[1433,-3479],[1153,-4894],[562,-474]],[[67829,617353],[-833,347],[-465,4024],[635,1566]],[[67166,623290],[-33,1550]],[[67133,624840],[1759,-1668],[1095,-2783]],[[69987,620389],[-1404,-1566]],[[68583,618823],[-754,-1470]],[[65314,628731],[1381,-1039]],[[66695,627692],[-1247,-826],[-134,1865]],[[63295,630407],[1393,-358]],[[64688,630049],[-410,-585]],[[64278,629464],[-983,943]],[[61668,631836],[456,-883]],[[62124,630953],[-1320,65]],[[60804,631018],[-453,1580]],[[60351,632598],[863,688]],[[61214,633286],[454,-1450]],[[57298,634654],[-1158,648],[1215,1054]],[[57355,636356],[-57,-1702]],[[248192,756583],[1342,-2445],[-500,-2774],[-1946,-1558],[262,-2094],[-1356,-3769]],[[245994,743943],[-804,1453],[-5606,-69],[-5598,-66]],[[233986,745261],[-894,7225],[-1095,4087]],[[231997,756573],[-391,1293],[467,4571]],[[232073,762437],[4516,2],[9952,6]],[[246541,762445],[488,-4229],[1163,-1633]],[[191524,768349],[3,-14800]],[[191527,753549],[-8312,2]],[[183215,753551],[-8261,-4]],[[174954,753547],[-10,10711],[301,2067],[-806,1652],[1813,6046],[79,1571],[-1045,1660]],[[175286,777254],[-356,2530],[-40,15190]],[[177643,794974],[-1,-5855],[875,-1768],[87,-1592],[981,-1046],[2077,-3560],[696,44],[-495,-6470],[643,-521],[986,1149],[1678,-5175],[942,-1941],[2134,476],[2032,-94],[450,1184],[796,-1456]],[[256077,756489],[41,-1692]],[[256118,754797],[767,-2866]],[[256885,751931],[-25,-16700],[-293,-2170],[-659,-1467],[-469,-2998]],[[255439,728596],[-182,-1509],[-1076,-1007],[64,-1635],[-1596,560],[-300,-1130]],[[252349,723875],[-924,1307],[-101,2558],[-1806,2515],[-558,1426],[687,3098],[-1491,918],[-710,2366],[-1407,2606],[-45,3274]],[[248192,756583],[7885,-94]],[[264455,751775],[-71,-15460]],[[264384,736315],[58,-1614],[-1789,-808],[-38,-905],[-1792,-3190],[-586,1018],[-650,-1461],[-890,350],[-756,-1013],[-642,701],[-1860,-797]],[[256885,751931],[1830,194]],[[258715,752125],[5739,-4],[1,-346]],[[237172,723926],[-5135,0],[-6419,0],[-8987,0]],[[216598,741701],[7569,0],[6405,0],[4575,3]],[[235147,741704],[1158,-829],[-355,-2089],[1210,-2279],[12,-12581]],[[270520,732502],[66,-2065],[858,-2444],[875,-874]],[[272319,727119],[-1990,-2379],[-1302,-2226],[-1437,-932]],[[267590,721582],[-175,-108],[-7714,462],[-3699,-124],[-611,-854],[-3865,1]],[[251526,720959],[817,1278],[6,1638]],[[264384,736315],[1288,-249],[363,-1094],[1493,-1278],[2248,360],[744,-1552]],[[251331,683591],[-1957,1106],[-522,-1415]],[[248852,683282],[661,-659]],[[249513,682623],[1216,846]],[[250729,683469],[1064,-2084],[-1018,-1190]],[[250775,680195],[576,-1180]],[[251351,679015],[1383,-1287]],[[252734,677728],[-939,-786],[-1454,2297],[-487,-157]],[[249854,679082],[-446,-1934]],[[249408,677148],[-463,1127]],[[248945,678275],[-1032,-973],[-1497,937],[127,996],[-1802,2244]],[[244741,681479],[-1021,-1654]],[[243720,679825],[-1141,239],[-1400,1077],[-1968,184]],[[239211,681325],[250,991]],[[239461,682316],[205,3442],[527,2891],[-1425,5646],[6,6000]],[[246806,700318],[0,-2316],[561,-2280],[-941,-2110],[-921,-3596],[-107,-1630],[5336,-4],[-277,-1602],[874,-3189]],[[302128,751805],[-426,1839],[-1149,-29]],[[295887,753873],[631,4128]],[[296518,758001],[2185,-130]],[[298703,757871],[3159,-165],[1454,1032]],[[303316,758738],[196,-1229]],[[303512,757509],[-863,-2003]],[[302649,755506],[856,-606],[621,-2521]],[[304126,752379],[1425,135]],[[305551,752514],[147,-883]],[[305698,751631],[-1968,-847],[-122,1071]],[[303608,751855],[-1299,-1336]],[[302309,750519],[-181,1286]],[[291562,732546],[-940,-2552]],[[290622,729994],[-787,-421]],[[289835,729573],[-532,105]],[[289303,729678],[-105,2275],[-897,34]],[[288301,731987],[-146,1414]],[[288155,733401],[731,10]],[[288886,733411],[-652,3495]],[[288234,736906],[1007,1892],[-1521,-1694],[-224,-4105]],[[287496,732999],[130,-2125]],[[287626,730874],[-2186,1904]],[[285440,732778],[587,2337]],[[286027,735115],[-1935,2708]],[[284092,737823],[-357,1501],[-905,509],[-873,-902],[-784,552],[-1973,-2463],[29,3033]],[[279229,740053],[10258,-3]],[[313542,772321],[-185,-2926]],[[313357,769395],[-1799,-588]],[[311558,768807],[-605,-1137]],[[310953,767670],[-1094,730]],[[309859,768400],[-228,-1475],[-1191,1038]],[[308440,767963],[-297,-1992]],[[308143,765971],[-2055,-1928]],[[306088,764043],[-436,492],[-2133,-4652]],[[303519,759883],[-618,1892],[-358,11284]],[[270430,756850],[-395,631]],[[270035,757481],[-1853,-5444]],[[268182,752037],[-3727,-262]],[[258715,752125],[639,894]],[[259354,753019],[925,2999]],[[260279,756018],[215,2861]],[[260494,758879],[-889,4365]],[[259605,763244],[64,2586]],[[259669,765830],[620,1522]],[[260289,767352],[164,2308],[1723,2801],[186,-2496],[483,2993]],[[262845,772958],[1135,685]],[[263980,773643],[-40,2219]],[[263940,775862],[582,164]],[[264522,776026],[3593,-2701]],[[268115,773325],[512,-3884]],[[268627,769441],[-101,-1826]],[[268526,767615],[-1523,-2444]],[[267003,765171],[399,-1990]],[[267402,763181],[979,1761]],[[268381,764942],[1210,848]],[[269591,765790],[788,-1261]],[[270379,764529],[684,-4958]],[[256650,771959],[-721,1537],[157,1886],[-952,1536],[-5464,2348],[-802,1488]],[[248868,780754],[2923,1713],[1957,2074]],[[253748,784541],[560,-2551]],[[254308,781990],[647,889]],[[254955,782879],[1606,-741],[777,-1790],[2002,-424],[1404,1387]],[[260744,781311],[3234,497]],[[263978,781808],[-185,-1570]],[[263793,780238],[1964,-86]],[[265757,780152],[279,-1823]],[[266036,778329],[906,-1200],[-1723,81],[-780,-712]],[[264439,776498],[-1881,1381],[-3012,-1333]],[[259546,776546],[-1141,-944]],[[258405,775602],[-23,1149]],[[258382,776751],[-1732,-4792]],[[254939,784414],[-604,-1184]],[[254335,783230],[-490,967]],[[253845,784197],[615,1271]],[[254460,785468],[1815,267]],[[256275,785735],[-1336,-1321]],[[251173,789100],[-3832,-2831]],[[247341,786269],[-3194,-4511]],[[244147,781758],[-513,-602],[-3,-3414],[-1114,-1040],[-553,-1861],[516,-598],[-256,-4170],[3935,-4735],[382,-2893]],[[232073,762437],[0,10641],[-1088,1770],[803,2055]],[[231788,776903],[-640,4178],[-200,5571],[-742,3466],[-278,4857]],[[251526,720959],[-706,-2952]],[[237171,720965],[1,2961]],[[235147,741704],[-1161,3557]],[[254445,684648],[-1404,262]],[[253041,684910],[-1710,-1319]],[[249184,712079],[5890,-12]],[[211080,776905],[-72,-5581]],[[211008,771324],[-4871,0],[-8525,0],[-6087,0],[-1,-2975]],[[211017,794974],[63,-18069]],[[281766,705417],[-3026,5452],[-3141,189],[-842,1937],[-3524,272],[-2108,-1186]],[[265765,712003],[104,1260],[1155,1860],[2447,1692],[285,896],[2323,1014],[882,1378],[208,1511]],[[273169,721614],[3063,-141],[6030,-100],[6720,-109]],[[288982,721264],[232,-2226]],[[289214,719038],[-2343,-1292]],[[286871,717746],[2650,-342]],[[289521,717404],[-4,-1498]],[[289517,715906],[-1111,-1735]],[[288406,714171],[-996,914]],[[287410,715085],[-1228,-297],[1282,-1113]],[[287464,713675],[-11,-2922]],[[287453,710753],[-1714,-411]],[[285739,710342],[-1714,-2505]],[[284025,707837],[-491,-2046]],[[283534,705791],[-1768,-374]],[[231788,776903],[-9060,0],[-7118,0],[-4530,2]],[[211051,747624],[-20,11852]],[[211031,759476],[9614,0],[5906,-2],[827,-881],[2514,49],[2105,-2069]],[[303519,759883],[-203,-1145]],[[298703,757871],[-230,926],[457,3807],[983,4571],[967,885],[460,3302]],[[291460,741597],[943,1013],[-1262,2615],[184,2389],[1177,2122]],[[292502,749736],[2192,-2162]],[[294694,747574],[-921,-3176]],[[293773,744398],[789,-758],[-607,-3565],[-1718,-4293]],[[292237,735782],[-2027,2893]],[[290210,738675],[284,1774],[966,1148]],[[213888,720966],[-116,-3],[-66,-26647],[-10007,-11],[619,-1378]],[[204318,692927],[-4907,65]],[[199411,692992],[-7,-2665]],[[199404,690327],[-2315,-10]],[[213886,723926],[2,-2960]],[[166664,753547],[8290,0]],[[183215,753551],[6,-29605]],[[300269,747979],[-3587,-2408]],[[296682,745571],[-2327,-92]],[[294355,745479],[1273,1665],[4641,835]],[[296244,771350],[86,-4365],[-195,-4089],[400,-151],[-17,-4744]],[[295471,747571],[-991,-1425],[214,1428]],[[292502,749736],[-899,1159],[-913,2639],[-4595,6],[-7659,10],[0,1627]],[[278436,755177],[2459,3101]],[[280895,758278],[-523,1818]],[[280385,761144],[2462,663]],[[282847,761807],[1794,-755],[1535,60]],[[286176,761112],[2069,1617],[-157,1768]],[[288088,764497],[604,887]],[[288692,765384],[-782,637]],[[287910,766021],[1540,2279]],[[276336,745528],[-1234,-6373],[-1928,-1773],[-626,-2238],[-329,693],[-881,-3149],[-818,-186]],[[268182,752037],[2706,-2059]],[[270888,749978],[2103,669]],[[272991,750647],[1374,1537]],[[274365,752184],[1967,1298]],[[276332,753482],[4,-7954]],[[237544,704065],[-2094,1826],[-1955,-437],[-1037,-1017],[-1841,1344],[-393,-1204],[-1503,1512],[-506,-676],[-711,1594],[-1098,-333],[-1924,869],[-483,1308],[-756,-401],[-1019,1174],[-9,11341],[-8327,1]],[[157719,778152],[775,-229],[601,-2614],[1453,-490],[1500,753],[1715,-468],[1927,510],[3699,1630],[5897,10]],[[154920,753549],[-351,723]],[[154569,754272],[-513,4087],[1086,5208]],[[155142,763567],[249,6434]],[[155391,770001],[361,4735]],[[155752,774736],[49,3585]],[[155801,778321],[1918,-169]],[[291460,741597],[-963,-995]],[[279229,740053],[-2895,-2],[2,5477]],[[276332,753482],[2104,1695]],[[302128,751805],[-583,-1497]],[[301545,750308],[-1107,-701]],[[281766,705417],[-770,-903]],[[280996,704514],[-1078,-3193]],[[279918,701321],[-2201,-3349]],[[277717,697972],[-947,-706]],[[276770,697266],[-1416,-2791]],[[211031,759476],[-23,11848]],[[267590,721582],[5579,32]],[[239461,682316],[-849,-1818]],[[238612,680498],[-1885,-726]],[[236727,679772],[101,1197],[-781,-282]],[[236047,680687],[374,-1965]],[[236421,678722],[-1070,-2410]],[[235351,676312],[-1612,-1917]],[[233739,674395],[-2185,405],[609,-1489]],[[232163,673311],[-1703,-2153]],[[230460,671158],[-707,-2508]],[[229753,668650],[-739,-4166],[424,-3382]],[[229438,661102],[711,-2578],[-590,-538]],[[229559,657986],[-2011,1148],[-2847,2266],[-933,3494],[-172,3032]],[[223596,667926],[-2196,4617]],[[221400,672543],[-1008,4389]],[[220392,676932],[-2171,4196],[-2508,523],[-1087,-1310]],[[214626,680341],[-364,-2286]],[[214262,678055],[-1089,-1522]],[[213173,676533],[-2369,2281]],[[210804,678814],[-1093,1727]],[[209711,680541],[-1319,5736],[-825,956]],[[207567,687233],[-2722,4369]],[[204845,691602],[-527,1325]],[[191527,753549],[0,-5925],[5566,0]],[[290622,729994],[-612,-2333]],[[290010,727661],[-1114,-2176]],[[288896,725485],[939,4088]],[[286027,735115],[-787,-2919]],[[285240,732196],[2122,-1790]],[[287362,730406],[793,-1190]],[[288155,729216],[2,-3179]],[[288157,726037],[-965,-204]],[[287192,725833],[910,-1599]],[[288102,724234],[-323,-965]],[[287779,723269],[1111,136],[92,-2141]],[[272319,727119],[1112,-1954],[1725,545],[1743,1231],[86,992],[1910,5332],[993,-596],[553,1856],[1683,2198],[314,1724],[1351,-1816],[303,1192]],[[157719,778152],[-677,696]],[[157042,778848],[-1689,49]],[[155353,778897],[-108,4478]],[[155245,783375],[-734,3693]],[[154511,787068],[-681,1455]],[[153830,788523],[-247,2821]],[[153583,791344],[2040,-1255]],[[155623,790089],[2642,-516]],[[158265,789573],[683,333]],[[158948,789906],[557,-5003]],[[159505,784903],[623,464],[-109,2660],[419,1128],[-1185,2692]],[[159253,791847],[344,1760]],[[159597,793607],[-677,1367]],[[258356,772744],[-801,-2405]],[[257555,770339],[-323,707]],[[257232,771046],[1124,1698]],[[256650,771959],[-1009,-3296]],[[255641,768663],[1013,1747]],[[256654,770410],[742,-421]],[[257396,769989],[-1568,-9070],[249,-4430]],[[244147,781758],[1689,23]],[[245836,781781],[1536,1111]],[[247372,782892],[581,-1569]],[[247953,781323],[915,-569]]],"transform":{"scale":[0.00036000036000036,0.00016879196566696583],"translate":[-180,-85.19218750000006]}} diff --git a/desktop/angular/.gitignore b/desktop/angular/.gitignore index 28f76669..d86cd691 100644 --- a/desktop/angular/.gitignore +++ b/desktop/angular/.gitignore @@ -1,4 +1,5 @@ node_modules dist dist-extension -dist-lib \ No newline at end of file +dist-lib +.angular \ No newline at end of file diff --git a/desktop/angular/README.md b/desktop/angular/README.md new file mode 100644 index 00000000..657f3808 --- /dev/null +++ b/desktop/angular/README.md @@ -0,0 +1,104 @@ +# Portmaster + +Welcome to the new Portmaster User-Interface. It's based on Angular and is built, unit and e2e tested using `@angular/cli`. + +## Running locally + +This section explains how to prepare your Ubuntu machine to build and test the new Portmaster User-Interface. It's recommended to use +a virtual machine but running it on bare metal will work as well. You can use the new Portmaster UI as well as the old one in parallel so +you can simply switch back when something is still missing or buggy. + +1. **Prepare your tooling** + +There's a simple dockerized way to build and test the new UI. Just make sure to have docker installed: + +```bash +sudo apt update +sudo apt install -y docker.io git +sudo systemctl enable --now docker +sudo gpasswd -a $USER docker +``` + +2. **Portmaster installation** + +Next, make sure to install the Portmaster using the official .deb installer from [here](https://updates.safing.io/latest/linux_amd64/packages/portmaster-installer.deb). See the [Wiki](https://github.com/safing/portmaster/wiki/Linux) for more information. + +Once the Portmaster is installed we need to add two new configuration flags. Execute the following: + +```bash +echo 'PORTMASTER_ARGS="--experimental-nfqueue --devmode"' | sudo tee /etc/default/portmaster +sudo systemctl daemon-reload +sudo systemctl restart portmaster +``` + +3. **Build and run the new UI** + +Now, clone this repository and execute the `docker.sh` script: + +```bash +# Clone the repository +git clone https://github.com/safing/portmaster-ui + +# Enter the repo and checkout the correct branch +cd portmaster-ui +git checkout feature/new-ui + +# Enter the directory and run docker.sh +cd modules/portmaster +sudo bash ./docker.sh +``` + +Finally open your browser and point it to http://localhost:8080. + +## Hacking Quick Start + +Although everything should work in the docker container as well, for the best development experience it's recommended to install `@angular/cli` locally. + +It's highly recommended to: +- Use [VSCode](https://code.visualstudio.com/) (or it's oss or server-side variant) with + - the official [Angular Language Service](https://marketplace.visualstudio.com/items?itemName=Angular.ng-template) extension + - the [Tailwind CSS Extension Pack](https://marketplace.visualstudio.com/items?itemName=andrewmcodes.tailwindcss-extension-pack) extension + - the [formate: CSS/LESS/SCSS formatter](https://github.com/mblander/formate) extension + +### Folder Structure + +From the project root (the folder containing this [README.md](./)) there are only two folders with the following content and structure: + +- **`src/`** contains the actual application sources: + - **`app/`** contains the actual application sources (components, services, uni tests ...) + - **`layout/`** contains components that form the overall application layout. For example the navigation bar and the side dash are located there. + - **`pages/`** contains the different pages of the application. A page is something that is associated with a dedicated application route and is rendered at the applications main content. + - **`services/`** contains shared services (like PortAPI and friends) + - **`shared/`** contains shared components that are likely used accross other components or pages. + - **`widgets/`** contains widgets and their settings components for the application side dash. + - **`debug/`** contains a debug sidebar component + - **`assets/`** contains static assets that must be shipped seperately. + - **`environments/`** contains build and production related environment settings (those are handled by `@angular/cli` automatically, see [angular.json](angular.json)) +- **`e2e/`** contains end-to-end testing sources. + + +### Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. + +In development mode (that is, you don't pass `--prod`) the UI expects portmaster running at `ws://127.0.0.1:817/api/database/v1`. See [environment](./src/app/environments/environment.ts). + +### Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +### Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. + +### Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +### Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). + +### Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git a/desktop/angular/angular.json b/desktop/angular/angular.json new file mode 100644 index 00000000..d99f44d0 --- /dev/null +++ b/desktop/angular/angular.json @@ -0,0 +1,457 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "portmaster": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + }, + "@schematics/angular:application": { + "strict": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.app.json", + "aot": true, + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/theme.less", + "src/styles.scss", + "node_modules/prismjs/themes/prism-okaidia.css", + "node_modules/prismjs/plugins/line-numbers/prism-line-numbers.css" + ], + "stylePreprocessorOptions": { + "includePaths": [ + "dist-lib/" + ] + }, + "scripts": [ + "node_modules/marked/marked.min.js", + "node_modules/emoji-toolkit/lib/js/joypixels.min.js", + "node_modules/prismjs/prism.js", + "node_modules/prismjs/components/prism-yaml.min.js", + "node_modules/prismjs/components/prism-json.min.js", + "node_modules/prismjs/plugins/line-numbers/prism-line-numbers.js" + ], + "vendorChunk": true, + "extractLicenses": false, + "buildOptimizer": false, + "sourceMap": true, + "optimization": false, + "namedChunks": true + }, + "configurations": { + "development": {}, + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "optimization": { + "scripts": true, + "styles": { + "minify": true, + "inlineCritical": false + } + }, + "outputHashing": "all", + "sourceMap": false, + "namedChunks": false, + "extractLicenses": true, + "vendorChunk": true, + "buildOptimizer": true, + "budgets": [ + { + "type": "initial", + "maximumWarning": "4mb", + "maximumError": "16mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4mb", + "maximumError": "16mb" + } + ] + } + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "portmaster:build" + }, + "configurations": { + "production": { + "browserTarget": "portmaster:build:production" + } + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "portmaster:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.spec.json", + "karmaConfig": "karma.conf.js", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.scss" + ], + "scripts": [] + } + }, + "e2e": { + "builder": "@angular-devkit/build-angular:protractor", + "options": { + "protractorConfig": "e2e/protractor.conf.js", + "devServerTarget": "portmaster:serve" + }, + "configurations": { + "production": { + "devServerTarget": "portmaster:serve:production" + } + } + }, + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": { + "lintFilePatterns": [ + "src/**/*.ts", + "src/**/*.html" + ] + } + } + } + }, + "@safing/ui": { + "projectType": "library", + "root": "projects/safing/ui", + "sourceRoot": "projects/safing/ui/src", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:ng-packagr", + "options": { + "project": "projects/safing/ui/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "projects/safing/ui/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "projects/safing/ui/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "projects/safing/ui/src/test.ts", + "tsConfig": "projects/safing/ui/tsconfig.spec.json", + "karmaConfig": "projects/safing/ui/karma.conf.js" + } + }, + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": { + "lintFilePatterns": [ + "projects/safing/ui/**/*.ts", + "projects/safing/ui/**/*.html" + ] + } + } + } + }, + "portmaster-chrome-extension": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "projects/portmaster-chrome-extension", + "sourceRoot": "projects/portmaster-chrome-extension/src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-builders/custom-webpack:browser", + "options": { + "customWebpackConfig": { + "path": "./browser-extension.config.ts" + }, + "outputPath": "dist-extension", + "index": "projects/portmaster-chrome-extension/src/index.html", + "main": "projects/portmaster-chrome-extension/src/main.ts", + "polyfills": "projects/portmaster-chrome-extension/src/polyfills.ts", + "tsConfig": "projects/portmaster-chrome-extension/tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "projects/portmaster-chrome-extension/src/favicon.ico", + "projects/portmaster-chrome-extension/src/assets", + "projects/portmaster-chrome-extension/src/manifest.json" + ], + "styles": [ + "projects/portmaster-chrome-extension/src/styles.scss" + ], + "scripts": [], + "optimization": { + "styles": { + "inlineCritical": false + } + }, + "outputHashing": "none" + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "fileReplacements": [ + { + "replace": "projects/portmaster-chrome-extension/src/environments/environment.ts", + "with": "projects/portmaster-chrome-extension/src/environments/environment.prod.ts" + } + ], + "outputHashing": "none" + }, + "development": { + "customWebpackConfig": { + "path": "./browser-extension-dev.config.ts" + }, + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "portmaster-chrome-extension:build:production" + }, + "development": { + "browserTarget": "portmaster-chrome-extension:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "portmaster-chrome-extension:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "projects/portmaster-chrome-extension/src/test.ts", + "polyfills": "projects/portmaster-chrome-extension/src/polyfills.ts", + "tsConfig": "projects/portmaster-chrome-extension/tsconfig.spec.json", + "karmaConfig": "projects/portmaster-chrome-extension/karma.conf.js", + "inlineStyleLanguage": "scss", + "assets": [ + "projects/portmaster-chrome-extension/src/favicon.ico", + "projects/portmaster-chrome-extension/src/assets" + ], + "styles": [ + "projects/portmaster-chrome-extension/src/styles.scss" + ], + "scripts": [] + } + } + } + }, + "@safing/portmaster-api": { + "projectType": "library", + "root": "projects/safing/portmaster-api", + "sourceRoot": "projects/safing/portmaster-api/src", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:ng-packagr", + "options": { + "project": "projects/safing/portmaster-api/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "projects/safing/portmaster-api/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "projects/safing/portmaster-api/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "projects/safing/portmaster-api/src/test.ts", + "tsConfig": "projects/safing/portmaster-api/tsconfig.spec.json", + "karmaConfig": "projects/safing/portmaster-api/karma.conf.js" + } + } + } + }, + "tauri-builtin": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "skipTests": true, + "style": "scss", + "standalone": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true, + "standalone": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true, + "standalone": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "projects/tauri-builtin", + "sourceRoot": "projects/tauri-builtin/src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/tauri-builtin", + "index": "projects/tauri-builtin/src/index.html", + "main": "projects/tauri-builtin/src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "projects/tauri-builtin/tsconfig.app.json", + "assets": [ + "projects/tauri-builtin/src/favicon.ico", + "projects/tauri-builtin/src/assets" + ], + "styles": [ + "projects/tauri-builtin/src/styles.scss" + ], + "inlineStyleLanguage": "scss", + "stylePreprocessorOptions": { + "includePaths": [ + "dist-lib/" + ] + }, + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "tauri-builtin:build:production" + }, + "development": { + "browserTarget": "tauri-builtin:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "tauri-builtin:build" + } + } + } + } + }, + "cli": { + "analytics": false + }, + "schematics": { + "@angular-eslint/schematics:application": { + "setParserOptionsProject": true + }, + "@angular-eslint/schematics:library": { + "setParserOptionsProject": true + } + } +} \ No newline at end of file diff --git a/desktop/angular/assets b/desktop/angular/assets new file mode 120000 index 00000000..41aef43f --- /dev/null +++ b/desktop/angular/assets @@ -0,0 +1 @@ +../../assets \ No newline at end of file diff --git a/desktop/angular/browser-extension-dev.config.ts b/desktop/angular/browser-extension-dev.config.ts new file mode 100644 index 00000000..a05dbcb1 --- /dev/null +++ b/desktop/angular/browser-extension-dev.config.ts @@ -0,0 +1,16 @@ +import type { Configuration } from 'webpack'; +const ExtensionReloader = require('webpack-ext-reloader'); +const config = require('./browser-extension.config'); + +module.exports = { + ...config, + mode: 'development', + plugins: [ + new ExtensionReloader({ + reloadPage: true, // Force the reload of the page also + entries: { // The entries used for the content/background scripts or extension pages + background: 'background', + } + }) + ] +} as Configuration; diff --git a/desktop/angular/browser-extension.config.ts b/desktop/angular/browser-extension.config.ts new file mode 100644 index 00000000..df5de5d3 --- /dev/null +++ b/desktop/angular/browser-extension.config.ts @@ -0,0 +1,5 @@ +import type { Configuration } from 'webpack'; + +module.exports = { + entry: { background: { import: 'projects/portmaster-chrome-extension/src/background.ts', runtime: false } }, +} as Configuration; diff --git a/desktop/angular/docker.sh b/desktop/angular/docker.sh new file mode 100755 index 00000000..bbd896e7 --- /dev/null +++ b/desktop/angular/docker.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# cd to script dir +baseDir="$( cd "$(dirname "$0")" && pwd )" +cd "$baseDir" + +# get base dir for mounting +mnt="$( cd ../.. && pwd )" + +# run container and start dev server +docker run \ + -ti \ + --rm \ + -v $mnt:/portmaster-ui \ + -w /portmaster-ui/modules/portmaster \ + -p 8081:8080 \ + node:latest \ + npm start -- --host 0.0.0.0 --port 8080 diff --git a/desktop/angular/e2e/protractor.conf.js b/desktop/angular/e2e/protractor.conf.js new file mode 100644 index 00000000..f238c0bb --- /dev/null +++ b/desktop/angular/e2e/protractor.conf.js @@ -0,0 +1,36 @@ +// @ts-check +// Protractor configuration file, see link for more information +// https://github.com/angular/protractor/blob/master/lib/config.ts + +const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); + +/** + * @type { import("protractor").Config } + */ +exports.config = { + allScriptsTimeout: 11000, + specs: [ + './src/**/*.e2e-spec.ts' + ], + capabilities: { + browserName: 'chrome' + }, + directConnect: true, + baseUrl: 'http://localhost:4200/', + framework: 'jasmine', + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000, + print: function() {} + }, + onPrepare() { + require('ts-node').register({ + project: require('path').join(__dirname, './tsconfig.json') + }); + jasmine.getEnv().addReporter(new SpecReporter({ + spec: { + displayStacktrace: StacktraceOption.PRETTY + } + })); + } +}; \ No newline at end of file diff --git a/desktop/angular/e2e/src/app.e2e-spec.ts b/desktop/angular/e2e/src/app.e2e-spec.ts new file mode 100644 index 00000000..ada7d128 --- /dev/null +++ b/desktop/angular/e2e/src/app.e2e-spec.ts @@ -0,0 +1,23 @@ +import { AppPage } from './app.po'; +import { browser, logging } from 'protractor'; + +describe('workspace-project App', () => { + let page: AppPage; + + beforeEach(() => { + page = new AppPage(); + }); + + it('should display welcome message', () => { + page.navigateTo(); + expect(page.getTitleText()).toEqual('portmaster app is running!'); + }); + + afterEach(async () => { + // Assert that there are no errors emitted from the browser + const logs = await browser.manage().logs().get(logging.Type.BROWSER); + expect(logs).not.toContain(jasmine.objectContaining({ + level: logging.Level.SEVERE, + } as logging.Entry)); + }); +}); diff --git a/desktop/angular/e2e/src/app.po.ts b/desktop/angular/e2e/src/app.po.ts new file mode 100644 index 00000000..b68475e0 --- /dev/null +++ b/desktop/angular/e2e/src/app.po.ts @@ -0,0 +1,11 @@ +import { browser, by, element } from 'protractor'; + +export class AppPage { + navigateTo(): Promise { + return browser.get(browser.baseUrl) as Promise; + } + + getTitleText(): Promise { + return element(by.css('app-root .content span')).getText() as Promise; + } +} diff --git a/desktop/angular/e2e/tsconfig.json b/desktop/angular/e2e/tsconfig.json new file mode 100644 index 00000000..426058ef --- /dev/null +++ b/desktop/angular/e2e/tsconfig.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/e2e", + "module": "commonjs", + "target": "es2018", + "types": [ + "jasmine", + "jasminewd2", + "node" + ] + } +} diff --git a/desktop/angular/karma.conf.js b/desktop/angular/karma.conf.js new file mode 100644 index 00000000..344d4317 --- /dev/null +++ b/desktop/angular/karma.conf.js @@ -0,0 +1,32 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, './coverage/portmaster'), + reports: ['html', 'lcovonly', 'text-summary'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/desktop/angular/package-lock.json b/desktop/angular/package-lock.json new file mode 100644 index 00000000..13f80b4f --- /dev/null +++ b/desktop/angular/package-lock.json @@ -0,0 +1,34959 @@ +{ + "name": "portmaster", + "version": "0.8.3", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "portmaster", + "version": "0.8.3", + "dependencies": { + "@angular/animations": "^16.0.1", + "@angular/cdk": "^16.0.1", + "@angular/common": "^16.0.1", + "@angular/compiler": "^16.0.1", + "@angular/core": "^16.0.1", + "@angular/forms": "^16.0.1", + "@angular/localize": "^16.0.1", + "@angular/platform-browser": "^16.0.1", + "@angular/platform-browser-dynamic": "^16.0.1", + "@angular/router": "^16.0.1", + "@fortawesome/angular-fontawesome": "^0.13.0", + "@fortawesome/fontawesome-svg-core": "^6.4.0", + "@fortawesome/free-brands-svg-icons": "^6.4.0", + "@fortawesome/free-regular-svg-icons": "^6.4.0", + "@fortawesome/free-solid-svg-icons": "^6.4.0", + "@tauri-apps/api": "^2.0.0-beta.3", + "@tauri-apps/plugin-cli": "^2.0.0-beta.1", + "@tauri-apps/plugin-clipboard-manager": "^2.0.0-alpha.4", + "@tauri-apps/plugin-dialog": "^2.0.0-alpha.4", + "@tauri-apps/plugin-notification": "^2.0.0-alpha.4", + "@tauri-apps/plugin-os": "^2.0.0-alpha.5", + "@tauri-apps/plugin-shell": "^2.0.0-alpha.4", + "autoprefixer": "^10.4.14", + "d3": "^7.8.4", + "data-urls": "^5.0.0", + "emoji-toolkit": "^7.0.1", + "fuse.js": "^6.6.2", + "ng-zorro-antd": "^16.1.0", + "ngx-markdown": "^16.0.0", + "postcss": "^8.4.23", + "prismjs": "^1.29.0", + "psl": "^1.9.0", + "rxjs": "~7.8.1", + "topojson-client": "^3.1.0", + "topojson-simplify": "^3.0.3", + "tslib": "^2.5.0", + "whatwg-encoding": "^3.1.1", + "zone.js": "^0.13.0" + }, + "devDependencies": { + "@angular-builders/custom-webpack": "^16.0.0-beta.1", + "@angular-devkit/build-angular": "^16.0.1", + "@angular-eslint/builder": "16.0.1", + "@angular-eslint/eslint-plugin": "16.0.1", + "@angular-eslint/eslint-plugin-template": "16.0.1", + "@angular-eslint/schematics": "16.0.1", + "@angular-eslint/template-parser": "16.0.1", + "@angular/cli": "^16.0.1", + "@angular/compiler-cli": "^16.0.1", + "@fullhuman/postcss-purgecss": "^5.0.0", + "@types/chrome": "^0.0.236", + "@types/d3": "^7.4.0", + "@types/data-urls": "^3.0.4", + "@types/jasmine": "^4.3.1", + "@types/jasminewd2": "~2.0.10", + "@types/node": "^20.1.5", + "@types/psl": "^1.1.0", + "@types/topojson-client": "^3.1.1", + "@types/topojson-simplify": "^3.0.1", + "@types/whatwg-encoding": "^2.0.3", + "@typescript-eslint/eslint-plugin": "^5.59.6", + "@typescript-eslint/parser": "^5.59.6", + "eslint": "^8.40.0", + "jasmine-core": "^5.0.0", + "jasmine-spec-reporter": "^7.0.0", + "js-yaml-loader": "^1.2.2", + "ng-packagr": "^16.0.1", + "npm-run-all": "^4.1.5", + "postcss-import": "^15.1.0", + "postcss-loader": "^7.3.0", + "postcss-scss": "^4.0.6", + "protractor": "~7.0.0", + "tailwindcss": "^3.3.2", + "ts-node": "^10.9.1", + "tslint": "~6.1.0", + "typescript": "4.9", + "webpack-bundle-analyzer": "^4.8.0", + "webpack-ext-reloader": "^1.1.9", + "zip-a-folder": "^1.1.5" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz", + "integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-builders/custom-webpack": { + "version": "16.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@angular-builders/custom-webpack/-/custom-webpack-16.0.0-beta.1.tgz", + "integrity": "sha512-C0tpgKJt++ciJ2nXtP2+fHOgzHUNyk5Su7bgTKY3yWMWlC9YfUMOlXHvNnCRUDaLqxXTsxQjGp56o9hPNd5miA==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": ">=0.1600.0 < 0.1700.0", + "@angular-devkit/build-angular": "^16.0.0", + "@angular-devkit/core": "^16.0.0", + "lodash": "^4.17.15", + "ts-node": "^10.0.0", + "tsconfig-paths": "^4.1.0", + "webpack-merge": "^5.7.3" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^16.0.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.0.2.tgz", + "integrity": "sha512-jh6ez6k1tPmLTQ8J2T0CY+aRqLbhCvaExH6pqB7q6/bkDItcLPrybDGfJf05F0dHvZPB2fQEK0xYz9i92POofQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "2.2.1", + "@angular-devkit/architect": "0.1600.2", + "@angular-devkit/build-webpack": "0.1600.2", + "@angular-devkit/core": "16.0.2", + "@babel/core": "7.21.4", + "@babel/generator": "7.21.4", + "@babel/helper-annotate-as-pure": "7.18.6", + "@babel/helper-split-export-declaration": "7.18.6", + "@babel/plugin-proposal-async-generator-functions": "7.20.7", + "@babel/plugin-transform-async-to-generator": "7.20.7", + "@babel/plugin-transform-runtime": "7.21.4", + "@babel/preset-env": "7.21.4", + "@babel/runtime": "7.21.0", + "@babel/template": "7.20.7", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "16.0.2", + "@vitejs/plugin-basic-ssl": "1.0.1", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.14", + "babel-loader": "9.1.2", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "4.21.5", + "cacache": "17.0.6", + "chokidar": "3.5.3", + "copy-webpack-plugin": "11.0.0", + "critters": "0.0.16", + "css-loader": "6.7.3", + "esbuild-wasm": "0.17.18", + "glob": "8.1.0", + "https-proxy-agent": "5.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "karma-source-map-support": "1.4.0", + "less": "4.1.3", + "less-loader": "11.1.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "magic-string": "0.30.0", + "mini-css-extract-plugin": "2.7.5", + "mrmime": "1.0.1", + "open": "8.4.2", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "2.3.1", + "piscina": "3.2.0", + "postcss": "8.4.23", + "postcss-loader": "7.2.4", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.62.1", + "sass-loader": "13.2.2", + "semver": "7.4.0", + "source-map-loader": "4.0.1", + "source-map-support": "0.5.21", + "terser": "5.17.1", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "tslib": "2.5.0", + "vite": "4.3.1", + "webpack": "5.80.0", + "webpack-dev-middleware": "6.0.2", + "webpack-dev-server": "4.13.2", + "webpack-merge": "5.8.0", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.17.18" + }, + "peerDependencies": { + "@angular/compiler-cli": "^16.0.0", + "@angular/localize": "^16.0.0", + "@angular/platform-server": "^16.0.0", + "@angular/service-worker": "^16.0.0", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^16.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=4.9.3 <5.1" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-webpack": { + "version": "0.1600.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1600.2.tgz", + "integrity": "sha512-B7EYoRMZOT3RcorxkXaHvMqwuNSttJCicZ99DmwBC41YlZOxpVVP6uM6wvYINGO0TMtu9bCmKkrSD8IC/hHetQ==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1600.2", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/core": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.0.2.tgz", + "integrity": "sha512-V4+t0BHO+QML9O2IiG2mJi8DtjeMOm4LAuG6tNDeiHZGAPOflvSPsKBtVl2JlXX/JxdLmyF4B6kRoAXRMKcwTg==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@ngtools/webpack": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.0.2.tgz", + "integrity": "sha512-8nPAOs2JLdMrAUf3sMkySzh66sPIkukO6HT8KVj726Dqm0Jtabjnxh0EI15Gkykj7HqH0Zw7/VyxpNQRfTA2UQ==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^16.0.0", + "typescript": ">=4.9.3 <5.1", + "webpack": "^5.54.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/postcss-loader": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.2.4.tgz", + "integrity": "sha512-F88rpxxNspo5hatIc+orYwZDtHFaVFOSIVAx+fBfJC1GmhWbVmPWtmg2gXKE1OxJbneOSGn8PWdIwsZFcruS+w==", + "dev": true, + "dependencies": { + "cosmiconfig": "^8.1.3", + "cosmiconfig-typescript-loader": "^4.3.0", + "klona": "^2.0.6", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "ts-node": ">=10", + "typescript": ">=4", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1600.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1600.2.tgz", + "integrity": "sha512-2AOP3/dwLywcjkRr3ixR/lb0uBn1jzaMWwQR3o7ye3IuEA2sRtyWhUzsy6V7smKBKWPDIbXvX2TcqYZAJ87ccA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "16.0.2", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/@angular-devkit/core": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.0.2.tgz", + "integrity": "sha512-V4+t0BHO+QML9O2IiG2mJi8DtjeMOm4LAuG6tNDeiHZGAPOflvSPsKBtVl2JlXX/JxdLmyF4B6kRoAXRMKcwTg==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.0.1.tgz", + "integrity": "sha512-VFhUViBfONOf6Ji4Lfkxlk+GN5l8Owm4Z0McqUIegrXsq3aSSStBBFdaDESpzhS6GIGqEBjjHMUQK8IlWT+EIQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "2.2.1", + "@angular-devkit/architect": "0.1600.1", + "@angular-devkit/build-webpack": "0.1600.1", + "@angular-devkit/core": "16.0.1", + "@babel/core": "7.21.4", + "@babel/generator": "7.21.4", + "@babel/helper-annotate-as-pure": "7.18.6", + "@babel/helper-split-export-declaration": "7.18.6", + "@babel/plugin-proposal-async-generator-functions": "7.20.7", + "@babel/plugin-transform-async-to-generator": "7.20.7", + "@babel/plugin-transform-runtime": "7.21.4", + "@babel/preset-env": "7.21.4", + "@babel/runtime": "7.21.0", + "@babel/template": "7.20.7", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "16.0.1", + "@vitejs/plugin-basic-ssl": "1.0.1", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.14", + "babel-loader": "9.1.2", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "4.21.5", + "cacache": "17.0.6", + "chokidar": "3.5.3", + "copy-webpack-plugin": "11.0.0", + "critters": "0.0.16", + "css-loader": "6.7.3", + "esbuild-wasm": "0.17.18", + "glob": "8.1.0", + "https-proxy-agent": "5.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "karma-source-map-support": "1.4.0", + "less": "4.1.3", + "less-loader": "11.1.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "magic-string": "0.30.0", + "mini-css-extract-plugin": "2.7.5", + "mrmime": "1.0.1", + "open": "8.4.2", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "2.3.1", + "piscina": "3.2.0", + "postcss": "8.4.23", + "postcss-loader": "7.2.4", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.62.1", + "sass-loader": "13.2.2", + "semver": "7.4.0", + "source-map-loader": "4.0.1", + "source-map-support": "0.5.21", + "terser": "5.17.1", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "tslib": "2.5.0", + "vite": "4.3.1", + "webpack": "5.80.0", + "webpack-dev-middleware": "6.0.2", + "webpack-dev-server": "4.13.2", + "webpack-merge": "5.8.0", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.17.18" + }, + "peerDependencies": { + "@angular/compiler-cli": "^16.0.0", + "@angular/localize": "^16.0.0", + "@angular/platform-server": "^16.0.0", + "@angular/service-worker": "^16.0.0", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^16.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=4.9.3 <5.1" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/architect": { + "version": "0.1600.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1600.1.tgz", + "integrity": "sha512-7N3Dugrp3Fyyn3Q6RsxFNJJ2m1QuqcF3GHJcX7siINL37Hp6xI/q5gKffcd9rf20H1DYZE0VIbR1Sk31G6hMWg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "16.0.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/postcss-loader": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.2.4.tgz", + "integrity": "sha512-F88rpxxNspo5hatIc+orYwZDtHFaVFOSIVAx+fBfJC1GmhWbVmPWtmg2gXKE1OxJbneOSGn8PWdIwsZFcruS+w==", + "dev": true, + "dependencies": { + "cosmiconfig": "^8.1.3", + "cosmiconfig-typescript-loader": "^4.3.0", + "klona": "^2.0.6", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "ts-node": ">=10", + "typescript": ">=4", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1600.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1600.1.tgz", + "integrity": "sha512-yCy5A1UwGzpst3QJ/CRo2Y8HWRqTPOfwAPAVl91Lbch7gBFViRvq6E7N1XfQunPu/eXvKxbuq2mFSDqtyZ1mWw==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1600.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/architect": { + "version": "0.1600.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1600.1.tgz", + "integrity": "sha512-7N3Dugrp3Fyyn3Q6RsxFNJJ2m1QuqcF3GHJcX7siINL37Hp6xI/q5gKffcd9rf20H1DYZE0VIbR1Sk31G6hMWg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "16.0.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.0.1.tgz", + "integrity": "sha512-2uz98IqkKJlgnHbWQ7VeL4pb+snGAZXIama2KXi+k9GsRntdcw+udX8rL3G9SdUGUF+m6+147Y1oRBMHsO/v4w==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.0.1.tgz", + "integrity": "sha512-A9D0LTYmiqiBa90GKcSuWb7hUouGIbm/AHbJbjL85WLLRbQA2PwKl7P5Mpd6nS/ZC0kfG4VQY3VOaDvb3qpI9g==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "16.0.1", + "jsonc-parser": "3.2.0", + "magic-string": "0.30.0", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-eslint/builder": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-16.0.1.tgz", + "integrity": "sha512-yjFltV+r3YjisVjASMPmWB/ASz39wdh0q5g0l6/4G+8yaxl6hEYs5o0ZOGeGdTFstCql8FGY+QKwKgsq9Ec4QQ==", + "dev": true, + "dependencies": { + "@nx/devkit": "16.0.2", + "nx": "16.0.2" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.0.1.tgz", + "integrity": "sha512-amvTgKHtZoygivW3LAYZ9qjLWsXM7/7eaRvaHdmAEdjyFnYQZ7UbWMPSQNz1mlW/AzTFvk9lGGQORglNOSDnww==", + "dev": true + }, + "node_modules/@angular-eslint/eslint-plugin": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-16.0.1.tgz", + "integrity": "sha512-CM9keS9cH1QAfSVfsvhw/oGCZcP/D8gfekWwVNjN/uEMEAak0czn1KOG7JQkE36NXOGtwCpTspMi1aa9CVKo9g==", + "dev": true, + "dependencies": { + "@angular-eslint/utils": "16.0.1", + "@typescript-eslint/utils": "5.59.2" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-16.0.1.tgz", + "integrity": "sha512-1hyfs+Iq7K2x3mDDE4985d8vDcMyknbE9HKHKUtRLfLKC9gnV3N5d4+UeySQ7Rrjvgzkc1g9qHADyuhwRWpDSA==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "16.0.1", + "@angular-eslint/utils": "16.0.1", + "@typescript-eslint/type-utils": "5.59.2", + "@typescript-eslint/utils": "5.59.2", + "aria-query": "5.1.3", + "axobject-query": "3.1.1" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/@typescript-eslint/scope-manager": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", + "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/@typescript-eslint/type-utils": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz", + "integrity": "sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.59.2", + "@typescript-eslint/utils": "5.59.2", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/@typescript-eslint/types": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", + "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", + "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/@typescript-eslint/utils": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", + "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", + "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/axobject-query": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", + "integrity": "sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==", + "dev": true, + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/@angular-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", + "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", + "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", + "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", + "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@angular-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", + "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular-eslint/eslint-plugin/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular-eslint/eslint-plugin/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@angular-eslint/eslint-plugin/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@angular-eslint/eslint-plugin/node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/@angular-eslint/schematics": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-16.0.1.tgz", + "integrity": "sha512-1oJJEWVbgPkNK1E8rAJfrgxzNWWzJKv3frTHeAm8gvZ7GftYhHjDcrcnxLWrYNxb9+q8Awi0hvGta/4HROmmnA==", + "dev": true, + "dependencies": { + "@angular-eslint/eslint-plugin": "16.0.1", + "@angular-eslint/eslint-plugin-template": "16.0.1", + "@nx/devkit": "16.0.2", + "ignore": "5.2.4", + "nx": "16.0.2", + "strip-json-comments": "3.1.1", + "tmp": "0.2.1" + }, + "peerDependencies": { + "@angular/cli": ">= 16.0.0 < 17.0.0" + } + }, + "node_modules/@angular-eslint/schematics/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/@angular-eslint/template-parser": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-16.0.1.tgz", + "integrity": "sha512-x0+SwSeqa3TiVZan6fE5grHsCkjGqU+zAS2DB6wAw5pyvgNAIjrI4cZEQ8pkgHfXe5tuumTKztlkpisah5s/hg==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "16.0.1", + "eslint-scope": "^7.0.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/template-parser/node_modules/eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@angular-eslint/template-parser/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@angular-eslint/utils": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-16.0.1.tgz", + "integrity": "sha512-2xnJuhIrMZEYK6UyBym6FaFXZgopIIbqfQ4sAtMWY6zYkCEsVUvx5qKIrsnXAwvpDQrv0WiMXteqi/5ICpVMZQ==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "16.0.1", + "@typescript-eslint/utils": "5.59.2" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/utils/node_modules/@typescript-eslint/scope-manager": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", + "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", + "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", + "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular-eslint/utils/node_modules/@typescript-eslint/utils": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", + "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@angular-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", + "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular-eslint/utils/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular-eslint/utils/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@angular-eslint/utils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@angular-eslint/utils/node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/@angular/animations": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.0.1.tgz", + "integrity": "sha512-ziRq1hGJJuQqQUHqNpEMp9uy1pVutvL8oNvawblh32u4bnLsVQU5gMd6sTonn0x4sphEwMNnuEmp/q6QRIx+pA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "16.0.1" + } + }, + "node_modules/@angular/cdk": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.0.1.tgz", + "integrity": "sha512-GupYss6x84RWEoy3JTYu4Igr2SxHuV6whVKMScQG2/Gm+winOsOn7YWm0IZQuFnjSWIF2Va5B0Tp0IjFHWxTvA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^16.0.0 || ^17.0.0", + "@angular/core": "^16.0.0 || ^17.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cli": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.0.1.tgz", + "integrity": "sha512-0vIAcq/S+3NXXN4/gBQFVGaxLUQ0zhRxxHQQuiT7GGII73UySuhwvaFB1BEhYG5HVJjRrP1F0ZYbvsvrmFzfXQ==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1600.1", + "@angular-devkit/core": "16.0.1", + "@angular-devkit/schematics": "16.0.1", + "@schematics/angular": "16.0.1", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "4.0.0", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "npm-package-arg": "10.1.0", + "npm-pick-manifest": "8.0.1", + "open": "8.4.2", + "ora": "5.4.1", + "pacote": "15.1.3", + "resolve": "1.22.2", + "semver": "7.4.0", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.1600.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1600.1.tgz", + "integrity": "sha512-7N3Dugrp3Fyyn3Q6RsxFNJJ2m1QuqcF3GHJcX7siINL37Hp6xI/q5gKffcd9rf20H1DYZE0VIbR1Sk31G6hMWg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "16.0.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.0.1.tgz", + "integrity": "sha512-ic9Ri4Mepf4c0BTff7o4Oyl/a1vACNXXUzuoTwIjWnIqrH89dtwg7ncTD9Rv0N1lon7r4gXokTbn9A/Yk/0jbw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "16.0.1", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.0.1.tgz", + "integrity": "sha512-7zNo6H1qVQow3T4EUul76SaIDSMRSl0hmtyWUzPjtWkxMjrCPSduqjA4/NHaG0KX1BsUvUtQEoDJ5jv/7EHWTQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "16.0.1" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.0.1.tgz", + "integrity": "sha512-EW7Oxp8EuTz3vCNd4RAncZGB7dCUYviUkBA4PzuyPmL2copZPt12j9qx0pXXF3T6ydjoZ+99ZEgfkKOV6FeU3g==", + "dependencies": { + "@babel/core": "7.19.3", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.1.2", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/compiler": "16.0.1", + "typescript": ">=4.9.3 <5.1" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", + "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.3", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.0", + "@babel/helpers": "^7.19.0", + "@babel/parser": "^7.19.3", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.3", + "@babel/types": "^7.19.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/generator": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz", + "integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==", + "dependencies": { + "@babel/types": "^7.21.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@angular/core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.0.1.tgz", + "integrity": "sha512-3s4XBbzWgyWcjI0WFlNDKRxsbm4J+OKIL4mJCM9r8gWwno9y0K/giziAm9YMIJ4VOBIvrcMbOh85o44FCk8cRA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.13.0" + } + }, + "node_modules/@angular/forms": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.0.1.tgz", + "integrity": "sha512-VbH/YnEBau0q97zI7BjSk0pu/i2S0Y/zmhvA2wgI2CCvtbqT6hCNdE/3rW6ZFBcnuCe+dFhuchXe6dX28epsvg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "16.0.1", + "@angular/core": "16.0.1", + "@angular/platform-browser": "16.0.1", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/localize": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.0.1.tgz", + "integrity": "sha512-2zC7KE/JUA/JCHP+kEDSF8iZ9cyvd6OAPFE74yH8FjixQsaq9WhXiPtGkHC0bg9hWH858bRcCmA9BZr+zjntvA==", + "dependencies": { + "@babel/core": "7.19.3", + "glob": "8.1.0", + "yargs": "^17.2.1" + }, + "bin": { + "localize-extract": "tools/bundles/src/extract/cli.js", + "localize-migrate": "tools/bundles/src/migrate/cli.js", + "localize-translate": "tools/bundles/src/translate/cli.js" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/compiler": "16.0.1", + "@angular/compiler-cli": "16.0.1" + } + }, + "node_modules/@angular/localize/node_modules/@babel/core": { + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", + "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.3", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.0", + "@babel/helpers": "^7.19.0", + "@babel/parser": "^7.19.3", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.3", + "@babel/types": "^7.19.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/localize/node_modules/@babel/generator": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz", + "integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==", + "dependencies": { + "@babel/types": "^7.21.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@angular/localize/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@angular/localize/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@angular/localize/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/localize/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/platform-browser": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.0.1.tgz", + "integrity": "sha512-7XLIOnTnGDJLE4Q0zBz6eI9q5V3NnsTAJqIICJHc4gk6jNgVz90gtejAQ4EFbo0d83XGzwFL22hxID5Dj1WRIA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/animations": "16.0.1", + "@angular/common": "16.0.1", + "@angular/core": "16.0.1" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.0.1.tgz", + "integrity": "sha512-qrGlRPqJM42WZcHCbzwTA8SiK90xrhM/VrOL/8/1okuHn82gSWbbynpqycdZnsI9XMbW+HNhpKR2n8HKV38Jug==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "16.0.1", + "@angular/compiler": "16.0.1", + "@angular/core": "16.0.1", + "@angular/platform-browser": "16.0.1" + } + }, + "node_modules/@angular/router": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.0.1.tgz", + "integrity": "sha512-4GH0SxPbuY08B/M0f3NEHf9yIFH+D3wlzWJHI75chfdqQ8gGAMG6B6PSmo3haicDxHcSnZTYNJXDLOQvaBAHcA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "16.0.1", + "@angular/core": "16.0.1", + "@angular/platform-browser": "16.0.1", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@ant-design/colors": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.0.0.tgz", + "integrity": "sha512-iVm/9PfGCbC0dSMBrz7oiEXZaaGH7ceU40OJEfKmyuzR9R5CRimJYPlRiFtMQGQcbNMea/ePcoIebi4ASGYXtg==", + "dependencies": { + "@ctrl/tinycolor": "^3.4.0" + } + }, + "node_modules/@ant-design/icons-angular": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/icons-angular/-/icons-angular-16.0.0.tgz", + "integrity": "sha512-KWBmWZl2so49R/MdAT7aG+xaBlMKl9SArR3Du/iPA0Am9GI1i9R89KgnnLWz+gkzHTye15S1IBXpgts4GPPU/w==", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": "^16.0.0", + "@angular/core": "^16.0.0", + "@angular/platform-browser": "^16.0.0", + "rxjs": "^6.4.0 || ^7.4.0" + } + }, + "node_modules/@assemblyscript/loader": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", + "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", + "dev": true + }, + "node_modules/@babel/code-frame": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", + "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.21.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.7.tgz", + "integrity": "sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz", + "integrity": "sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.21.4", + "@babel/helper-compilation-targets": "^7.21.4", + "@babel/helper-module-transforms": "^7.21.2", + "@babel/helpers": "^7.21.0", + "@babel/parser": "^7.21.4", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.4", + "@babel/types": "^7.21.4", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz", + "integrity": "sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==", + "dependencies": { + "@babel/types": "^7.21.4", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.21.5.tgz", + "integrity": "sha512-uNrjKztPLkUk7bpCNC0jEKDJzzkvel/W+HguzbN8krA+LPfC1CEobJEvAvGka2A/M+ViOqXdcRL0GqPUJSjx9g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz", + "integrity": "sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==", + "dependencies": { + "@babel/compat-data": "^7.21.5", + "@babel/helper-validator-option": "^7.21.0", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.21.8", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.8.tgz", + "integrity": "sha512-+THiN8MqiH2AczyuZrnrKL6cAxFRRQDKW9h1YkBvbgKmAm6mwiacig1qT73DHIWMGo40GRnsEfN3LA+E6NtmSw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.21.5", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-member-expression-to-functions": "^7.21.5", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.21.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/helper-split-export-declaration": "^7.18.6", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.21.8", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.8.tgz", + "integrity": "sha512-zGuSdedkFtsFHGbexAvNuipg1hbtitDLo2XE8/uf6Y9sOQV1xsYX/2pNbtedp/X0eU1pIt+kGvaqHCowkRbS5g==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "regexpu-core": "^5.3.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz", + "integrity": "sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", + "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "dependencies": { + "@babel/template": "^7.20.7", + "@babel/types": "^7.21.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.5.tgz", + "integrity": "sha512-nIcGfgwpH2u4n9GG1HpStW5Ogx7x7ekiFHbjjFRKXbn5zUvqO9ZgotCO4x1aNbKn/x/xOUaXEhyNHCwtFCpxWg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", + "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", + "dependencies": { + "@babel/types": "^7.21.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz", + "integrity": "sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.21.5", + "@babel/helper-module-imports": "^7.21.4", + "@babel/helper-simple-access": "^7.21.5", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.5", + "@babel/types": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz", + "integrity": "sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", + "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-wrap-function": "^7.18.9", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.21.5.tgz", + "integrity": "sha512-/y7vBgsr9Idu4M6MprbOVUfH3vs7tsIfnVWv/Ml2xgwvyH6LTngdfbf5AdsKwkJy4zgy1X/kuNrEKvhhK28Yrg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.21.5", + "@babel/helper-member-expression-to-functions": "^7.21.5", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.5", + "@babel/types": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz", + "integrity": "sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==", + "dependencies": { + "@babel/types": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", + "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", + "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", + "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.19.0", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.5.tgz", + "integrity": "sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==", + "dependencies": { + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.5", + "@babel/types": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.21.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.8.tgz", + "integrity": "sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", + "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", + "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-proposal-optional-chaining": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", + "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz", + "integrity": "sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", + "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.21.5.tgz", + "integrity": "sha512-wb1mhwGOCaXHDTcsRYMKF9e5bbMgqwxtqa2Y1ifH96dXJPwbuLX9qHy3clhrxVqgMz7nyNXs8VkxdH8UBcjKqA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", + "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", + "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz", + "integrity": "sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz", + "integrity": "sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-split-export-declaration": "^7.18.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.21.5.tgz", + "integrity": "sha512-TR653Ki3pAwxBxUe8srfF3e4Pe3FTA46uaNHYyQwIoM4oWKSoOZiDNyHJ0oIoDIUPSRQbQG7jzgVBX3FPVne1Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/template": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz", + "integrity": "sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", + "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", + "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", + "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.5.tgz", + "integrity": "sha512-nYWpjKW/7j/I/mZkGVgHJXh4bA1sfdFnJoOXwJuj4m3Q2EraO/8ZyrkCau9P5tbHQk01RMSt6KYLCsW7730SXQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", + "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", + "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", + "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", + "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.5.tgz", + "integrity": "sha512-OVryBEgKUbtqMoB7eG2rs6UFexJi6Zj6FDXx+esBLPTCxCNxAY9o+8Di7IsUGJ+AVhp5ncK0fxWUBd0/1gPhrQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.21.5", + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/helper-simple-access": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", + "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-identifier": "^7.19.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", + "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", + "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", + "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", + "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz", + "integrity": "sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", + "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.21.5.tgz", + "integrity": "sha512-ZoYBKDb6LyMi5yCsByQ5jmXsHAQDDYeexT1Szvlmui+lADvfSecr5Dxd/PkrTC3pAD182Fcju1VQkB4oCp9M+w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5", + "regenerator-transform": "^0.15.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", + "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.21.4.tgz", + "integrity": "sha512-1J4dhrw1h1PqnNNpzwxQ2UBymJUF8KuPjAAnlLwZcGhHAIqUigFW7cdK6GHoB64ubY4qXQNYknoUeks4Wz7CUA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.21.4", + "@babel/helper-plugin-utils": "^7.20.2", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", + "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", + "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", + "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", + "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", + "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.21.5.tgz", + "integrity": "sha512-LYm/gTOwZqsYohlvFUe/8Tujz75LqqVC2w+2qPHLR+WyWHGCZPN1KBpJCJn+4Bk4gOkQy/IXKIge6az5MqwlOg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", + "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.21.4.tgz", + "integrity": "sha512-2W57zHs2yDLm6GD5ZpvNn71lZ0B/iypSdIeq25OurDKji6AdzV07qp4s3n1/x5BqtiGaTrPN3nerlSCaC5qNTw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.21.4", + "@babel/helper-compilation-targets": "^7.21.4", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.21.0", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.20.7", + "@babel/plugin-proposal-async-generator-functions": "^7.20.7", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.21.0", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.20.7", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.20.7", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.21.0", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.21.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.20.7", + "@babel/plugin-transform-async-to-generator": "^7.20.7", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.21.0", + "@babel/plugin-transform-classes": "^7.21.0", + "@babel/plugin-transform-computed-properties": "^7.20.7", + "@babel/plugin-transform-destructuring": "^7.21.3", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.21.0", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.20.11", + "@babel/plugin-transform-modules-commonjs": "^7.21.2", + "@babel/plugin-transform-modules-systemjs": "^7.20.11", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.20.5", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.21.3", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.20.5", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.20.7", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.21.4", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", + "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.5.tgz", + "integrity": "sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==", + "dependencies": { + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.21.5", + "@babel/helper-environment-visitor": "^7.21.5", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.21.5", + "@babel/types": "^7.21.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz", + "integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==", + "dependencies": { + "@babel/types": "^7.21.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.5.tgz", + "integrity": "sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==", + "dependencies": { + "@babel/helper-string-parser": "^7.21.5", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz", + "integrity": "sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==", + "optional": true + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.18.tgz", + "integrity": "sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.18.tgz", + "integrity": "sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.18.tgz", + "integrity": "sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.18.tgz", + "integrity": "sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.18.tgz", + "integrity": "sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.18.tgz", + "integrity": "sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.18.tgz", + "integrity": "sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.18.tgz", + "integrity": "sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.18.tgz", + "integrity": "sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.18.tgz", + "integrity": "sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.18.tgz", + "integrity": "sha512-bvPG+MyFs5ZlwYclCG1D744oHk1Pv7j8psF5TfYx7otCVmcJsEXgFEhQkbhNW8otDHL1a2KDINW20cfCgnzgMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.18.tgz", + "integrity": "sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.18.tgz", + "integrity": "sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.18.tgz", + "integrity": "sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.18.tgz", + "integrity": "sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.18.tgz", + "integrity": "sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.18.tgz", + "integrity": "sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.18.tgz", + "integrity": "sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.18.tgz", + "integrity": "sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.18.tgz", + "integrity": "sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.18.tgz", + "integrity": "sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.18.tgz", + "integrity": "sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", + "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.2", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", + "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fortawesome/angular-fontawesome": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.13.0.tgz", + "integrity": "sha512-gzSPRdveOXNO7NIiMgTyB46aiHG0i98KinnAEqHXi8qzraM/kCcHn/0y3f4MhemX6kftwsFli0IU8RyHmtXlSQ==", + "dependencies": { + "tslib": "^2.4.1" + }, + "peerDependencies": { + "@angular/core": "^16.0.0", + "@fortawesome/fontawesome-svg-core": "~1.2.27 || ~1.3.0-beta2 || ^6.1.0" + } + }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz", + "integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.0.tgz", + "integrity": "sha512-Bertv8xOiVELz5raB2FlXDPKt+m94MQ3JgDfsVbrqNpLU9+UE2E18GKjLKw+d3XbeYPqg1pzyQKGsrzbw+pPaw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.0.tgz", + "integrity": "sha512-qvxTCo0FQ5k2N+VCXb/PZQ+QMhqRVM4OORiO6MXdG6bKolIojGU/srQ1ptvKk0JTbRgaJOfL2qMqGvBEZG7Z6g==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.0.tgz", + "integrity": "sha512-ZfycI7D0KWPZtf7wtMFnQxs8qjBXArRzczABuMQqecA/nXohquJ5J/RCR77PmY5qGWkxAZDxpnUFVXKwtY/jPw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.0.tgz", + "integrity": "sha512-kutPeRGWm8V5dltFP1zGjQOEAzaLZj4StdQhWVZnfGFCvAPVvHh8qk5bRrU4KXnRRRNni5tKQI9PBAdI6MP8nQ==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fullhuman/postcss-purgecss": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-5.0.0.tgz", + "integrity": "sha512-onDS/b/2pMRzqSoj4qOs2tYFmOpaspjTAgvACIHMPiicu1ptajiBruTrjBzTKdxWdX0ldaBb7wj8nEaTLyFkJw==", + "dev": true, + "dependencies": { + "purgecss": "^5.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", + "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "dev": true + }, + "node_modules/@ngtools/webpack": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.0.1.tgz", + "integrity": "sha512-CZHFPMiJuOe241kO1VSSPOQ5Z9hWWkY7eSs3hnS50Ntgd4YzlHAydqexmEFpXD2YLOFjdbNETCyJ2BQTM4Kwtw==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^16.0.0", + "typescript": ">=4.9.3 <5.1", + "webpack": "^5.54.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", + "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.0.4.tgz", + "integrity": "sha512-5yZghx+u5M47LghaybLCkdSyFzV/w4OuH12d96HO389Ik9CDsLaDZJVynSGGVJOLn6gy/k7Dz5XYcplM3uxXRg==", + "dev": true, + "dependencies": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.0.2.tgz", + "integrity": "sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ==", + "dev": true, + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "lib/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz", + "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==", + "dev": true, + "dependencies": { + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-6.0.2.tgz", + "integrity": "sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==", + "dev": true, + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/promise-spawn": "^6.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^3.0.0", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@nrwl/devkit": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-16.0.2.tgz", + "integrity": "sha512-SAEcImeQHdSTauO05FUn2vVl9/y5Kx1LNCZ4YE+SdY5/QRq18fuo/DCWmjOGG9M8r06vYGsAgMzkiB4soimcyA==", + "dev": true, + "dependencies": { + "@nx/devkit": "16.0.2" + } + }, + "node_modules/@nrwl/tao": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-16.0.2.tgz", + "integrity": "sha512-wimEe4OTpI7/nDK67RnpZpEXCU+fzA0sDgpIhMgbpPd0vPmKgaZv4nbs8zrm0goFlacmmnLaGRhhGYMOxE+1Lg==", + "dev": true, + "dependencies": { + "nx": "16.0.2" + }, + "bin": { + "tao": "index.js" + } + }, + "node_modules/@nx/devkit": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-16.0.2.tgz", + "integrity": "sha512-BY1Bj0BbAl6XJL0O+QGTWPs/3WMJTEQ+Y4Lfoq4dZM7RllE6rAylr54NA2wa4lsgordZhq1+0g5PVhKKvSVRRw==", + "dev": true, + "dependencies": { + "@nrwl/devkit": "16.0.2", + "ejs": "^3.1.7", + "ignore": "^5.0.4", + "semver": "7.3.4", + "tmp": "~0.2.1", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "nx": ">= 15 <= 17" + } + }, + "node_modules/@nx/devkit/node_modules/semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nx/devkit/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/@nx/nx-darwin-arm64": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-16.0.2.tgz", + "integrity": "sha512-nAT8WJ/qKGEvUcoFLHHye1dbwCd7b8CTZJlDF+ZkyCD/UZRHt4eJxy8gvKmxgkZTFb2+PPMQt4UORCUGpZzuoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-darwin-x64": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-16.0.2.tgz", + "integrity": "sha512-r0rfOrZaOyrwFR5a0UT05xkYRumfkP65cRSZM1TjCA027AG9llYtkLT1hlz8uMKt+P12zrWVzXSqGLDi022ZZg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm-gnueabihf": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-16.0.2.tgz", + "integrity": "sha512-TfDQaGvCIDjn9sPg5U1Fr2rsSul/4PIQB59qrLBJRPiCWgpzwO71Il1qwSX68En+JH3lwXr+g5EjcDIEQ8fGYA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm64-gnu": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-16.0.2.tgz", + "integrity": "sha512-MICaUp7uz8WVQFXWPrmQaX1o4bdL7f3C7b3MDDf6+Zau6RcyQuw97UEKaYi9OqrV3w8yuPplqoLosFblAgb8uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm64-musl": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-16.0.2.tgz", + "integrity": "sha512-wcBURG+6A2srm+6ujj8SShjwmYWs0eHI5D8vgZr8Bni+lXbKP/IosE9JGXKtRoh27/owyR8PGHhDVzjv46tlFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-x64-gnu": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-16.0.2.tgz", + "integrity": "sha512-Xyml2gFdVDHUj2g67DKz2aD78x1BciN1ZaaBTCxXL4MHfwR78SZa7mtRtE+1kj5OgVIwupZP50jq7C8GuSn3Hw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-x64-musl": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-16.0.2.tgz", + "integrity": "sha512-j3xdN8I5DlTgW5N5eCquyBZswrrYf6EazUCvnEpeejygwh3N6XN7DlD68Bs0CB4Zmd0tWLfTjNVAtUJSP6g2mA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-win32-arm64-msvc": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-16.0.2.tgz", + "integrity": "sha512-R2pzoW3SUFBbe9C1vifJnXuysPl6kmutQHN2yQ9lwJptzPvMxfDU1FuXmKCGRUGmEwFxk/XPhwDL/ZcbABTrzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-win32-x64-msvc": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-16.0.2.tgz", + "integrity": "sha512-r4H/SsqfpIJa8QLSpnscgkMnLsnkRYXj8TcILDrf+nJazfEdJZLUvVhN9O85OB7pskv86NuGfnJmJHHXy6QVQg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.0.4.tgz", + "integrity": "sha512-cTDi+FUDBIUOBKEtj+nhiJ71AZVlkAsQFuGQTun5tV9mwQBQgZvhCzG+URPQc8myeN32yRVZEfVAPCs1RW+Jvg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^3.2.1", + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, + "node_modules/@rollup/plugin-json": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.0.0.tgz", + "integrity": "sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.2.tgz", + "integrity": "sha512-Y35fRGUjC3FaurG722uhUuG8YHOJRJQbI6/CkbRkdPotSpDj9NtIN85z1zrcyDcCQIW4qp5mgG72U+gJ0TAFEg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.1", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", + "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@schematics/angular": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.0.1.tgz", + "integrity": "sha512-MNgH/iB3WWxMLFVHJjtXCHZ8YHtfx2e3mX2Ds5P43OTgSnTk6tHabqvwxJ4wzjoyoPUyXWLhHt0diCmVtDTNeQ==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "16.0.1", + "@angular-devkit/schematics": "16.0.1", + "jsonc-parser": "3.2.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.1.0.tgz", + "integrity": "sha512-a31EnjuIDSX8IXBUib3cYLDRlPMU36AWX4xS8ysLaNu4ZzUesDiPt83pgrW2X1YLMe5L2HbDyaKK5BrL4cNKaQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tauri-apps/api": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-beta.3.tgz", + "integrity": "sha512-gDSJzKpBs6efXw2ZWqjl9QVNImY5GR5qygXqB7JK4y7prcQInxnTj2ARFR0vD4wuzkrUHGrlIKraiJJPHWJ9vg==", + "engines": { + "node": ">= 18", + "npm": ">= 6.6.0", + "yarn": ">= 1.19.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/plugin-cli": { + "version": "2.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-cli/-/plugin-cli-2.0.0-beta.1.tgz", + "integrity": "sha512-8VB0RTFi6SrCZvWDiOW+DVhCo7IsBenWfTIF6f8YAU+TnLSOAxpVc2MOM5PimVdKU2hu+mlpjSmPhd9RSCRfAw==", + "dependencies": { + "@tauri-apps/api": "2.0.0-beta.2" + } + }, + "node_modules/@tauri-apps/plugin-cli/node_modules/@tauri-apps/api": { + "version": "2.0.0-beta.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-beta.2.tgz", + "integrity": "sha512-4r1r6kgttzIWxJ3HxkZQH+b7EiUtKhdUCPbi0KSalD+2T3j6klw+v8VyxhKwEdjM/eo60NE+J33v1E/Urq8puw==", + "engines": { + "node": ">= 18", + "npm": ">= 6.6.0", + "yarn": ">= 1.19.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/plugin-clipboard-manager": { + "version": "2.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0-alpha.4.tgz", + "integrity": "sha512-/xPQBXuzD8cSh81xkTphIAKxSD2kGsv8deKK+Qoh+89puay1xJjjnxVv5b9IKKn0G8r8HPm+JDEamlKxQbOgnA==", + "dependencies": { + "@tauri-apps/api": "2.0.0-alpha.12" + } + }, + "node_modules/@tauri-apps/plugin-clipboard-manager/node_modules/@tauri-apps/api": { + "version": "2.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-alpha.12.tgz", + "integrity": "sha512-acpNZQxFgHMHC5qV/IUg4IL/xmypzfxHB4ECkwb58fT48H4zBmklNd5TC0k7BvLUBoSmmgHc4InbYwQai392Yw==", + "engines": { + "node": ">= 18", + "npm": ">= 6.6.0", + "yarn": ">= 1.19.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0-alpha.4.tgz", + "integrity": "sha512-4NxBgDzxrZ8hPE9OMRYwsXYN2BxQYI/5l1UKEI5V4srFTZK81Vj5GGksCf7gQREZg7CmBRCk95qYx338A6oCag==", + "dependencies": { + "@tauri-apps/api": "2.0.0-alpha.12" + } + }, + "node_modules/@tauri-apps/plugin-dialog/node_modules/@tauri-apps/api": { + "version": "2.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-alpha.12.tgz", + "integrity": "sha512-acpNZQxFgHMHC5qV/IUg4IL/xmypzfxHB4ECkwb58fT48H4zBmklNd5TC0k7BvLUBoSmmgHc4InbYwQai392Yw==", + "engines": { + "node": ">= 18", + "npm": ">= 6.6.0", + "yarn": ">= 1.19.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/plugin-notification": { + "version": "2.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.0.0-alpha.4.tgz", + "integrity": "sha512-mXUuZoZEEMAedGNJxPZPLET3vY4lSmHCpfrfZIytJRU6eSxbec90L3fB4YqvW9+yqkplyXkvpiThILbT5A4Q4w==", + "dependencies": { + "@tauri-apps/api": "2.0.0-alpha.12" + } + }, + "node_modules/@tauri-apps/plugin-notification/node_modules/@tauri-apps/api": { + "version": "2.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-alpha.12.tgz", + "integrity": "sha512-acpNZQxFgHMHC5qV/IUg4IL/xmypzfxHB4ECkwb58fT48H4zBmklNd5TC0k7BvLUBoSmmgHc4InbYwQai392Yw==", + "engines": { + "node": ">= 18", + "npm": ">= 6.6.0", + "yarn": ">= 1.19.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/plugin-os": { + "version": "2.0.0-alpha.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0-alpha.5.tgz", + "integrity": "sha512-dedPdad+ykMSZz2KUfrhUDyy32G2WH5aLkYdcACF58KC6GBvKuyR5sQ1ZE/pddo2L6VRhyujLp8zJEfRN3AUcQ==", + "dependencies": { + "@tauri-apps/api": "2.0.0-alpha.12" + } + }, + "node_modules/@tauri-apps/plugin-os/node_modules/@tauri-apps/api": { + "version": "2.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-alpha.12.tgz", + "integrity": "sha512-acpNZQxFgHMHC5qV/IUg4IL/xmypzfxHB4ECkwb58fT48H4zBmklNd5TC0k7BvLUBoSmmgHc4InbYwQai392Yw==", + "engines": { + "node": ">= 18", + "npm": ">= 6.6.0", + "yarn": ">= 1.19.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/plugin-shell": { + "version": "2.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-alpha.4.tgz", + "integrity": "sha512-Go/+EwGVuAXbSg2l2M5E2gT6cir66KV4CXC9P4gPHeead8Ar/B9wQvuINzcrYzL/HCcL7fFfKlqqu/XPTN2qvQ==", + "dependencies": { + "@tauri-apps/api": "2.0.0-alpha.12" + } + }, + "node_modules/@tauri-apps/plugin-shell/node_modules/@tauri-apps/api": { + "version": "2.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-alpha.12.tgz", + "integrity": "sha512-acpNZQxFgHMHC5qV/IUg4IL/xmypzfxHB4ECkwb58fT48H4zBmklNd5TC0k7BvLUBoSmmgHc4InbYwQai392Yw==", + "engines": { + "node": ">= 18", + "npm": ">= 6.6.0", + "yarn": ">= 1.19.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, + "node_modules/@tufjs/canonical-json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz", + "integrity": "sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-1.0.4.tgz", + "integrity": "sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A==", + "dev": true, + "dependencies": { + "@tufjs/canonical-json": "1.0.0", + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.0.tgz", + "integrity": "sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chrome": { + "version": "0.0.236", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.236.tgz", + "integrity": "sha512-ArQoxO9WtDY6GWcT2cpo+D+hyASPeFt7PHQEUDXwQhRS00Rbop07rnEOA046yws0HkM83Tcew/hW6Dgvnj4iMQ==", + "dev": true, + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", + "integrity": "sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@types/cors": { + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/d3": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.0.tgz", + "integrity": "sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.2.tgz", + "integrity": "sha512-5mjGjz6XOXKOCdTajXTZ/pMsg236RdiwKPrRPWAEf/2S/+PzwY+LLYShUpeysWaMvsdS7LArh6GdUefoxpchsQ==", + "dev": true + }, + "node_modules/@types/d3-axis": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.1.tgz", + "integrity": "sha512-zji/iIbdd49g9WN0aIsGcwcTBUkgLsCSwB+uH+LPVDAiKWENMtI3cJEWt+7/YYwelMoZmbBfzA3qCdrZ2XFNnw==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.1.tgz", + "integrity": "sha512-B532DozsiTuQMHu2YChdZU0qsFJSio3Q6jmBYGYNp3gMDzBmuFFgPt9qKA4VYuLZMp4qc6eX7IUFUEsvHiXZAw==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-eQfcxIHrg7V++W8Qxn6QkqBNBokyhdWSAS73AbkbMzvLQmVVBviknoz2SRS/ZJdIOmhcmmdCRE/NFOm28Z1AMw==", + "dev": true + }, + "node_modules/@types/d3-color": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.0.2.tgz", + "integrity": "sha512-WVx6zBiz4sWlboCy7TCgjeyHpNjMsoF36yaagny1uXfbadc9f+5BeBf7U+lRmQqY3EHbGQpP8UdW8AC+cywSwQ==", + "dev": true + }, + "node_modules/@types/d3-contour": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.1.tgz", + "integrity": "sha512-C3zfBrhHZvrpAAK3YXqLWVAGo87A4SvJ83Q/zVJ8rFWJdKejUnDYaWZPkA8K84kb2vDA/g90LTQAz7etXcgoQQ==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.0.tgz", + "integrity": "sha512-iGm7ZaGLq11RK3e69VeMM6Oqj2SjKUB9Qhcyd1zIcqn2uE8w9GFB445yCY46NOQO3ByaNyktX1DK+Etz7ZaX+w==", + "dev": true + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-NhxMn3bAkqhjoxabVJWKryhnZXXYYVQxaBnbANu0O94+O/nX9qSjrA1P1jbAQJxJf+VC72TxDX/YJcKue5bRqw==", + "dev": true + }, + "node_modules/@types/d3-drag": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.1.tgz", + "integrity": "sha512-o1Va7bLwwk6h03+nSM8dpaGEYnoIG19P0lKqlic8Un36ymh9NSkNFX1yiXMKNMx8rJ0Kfnn2eovuFaL6Jvj0zA==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.0.tgz", + "integrity": "sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A==", + "dev": true + }, + "node_modules/@types/d3-ease": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz", + "integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==", + "dev": true + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-toZJNOwrOIqz7Oh6Q7l2zkaNfXkfR7mFSJvGvlD/Ciq/+SQ39d5gynHJZ/0fjt83ec3WL7+u3ssqIijQtBISsw==", + "dev": true, + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.3.tgz", + "integrity": "sha512-z8GteGVfkWJMKsx6hwC3SiTSLspL98VNpmvLpEFJQpZPq6xpA1I8HNBDNSpukfK0Vb0l64zGFhzunLgEAcBWSA==", + "dev": true + }, + "node_modules/@types/d3-format": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", + "dev": true + }, + "node_modules/@types/d3-geo": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.2.tgz", + "integrity": "sha512-DbqK7MLYA8LpyHQfv6Klz0426bQEf7bRTvhMy44sNGVyZoWn//B0c+Qbeg8Osi2Obdc9BLLXYAKpyWege2/7LQ==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.0.2.tgz", + "integrity": "sha512-+krnrWOZ+aQB6v+E+jEkmkAx9HvsNAD+1LCD0vlBY3t+HwjKnsBFbpVLx6WWzDzCIuiTWdAxXMEnGnVXpB09qQ==", + "dev": true + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "dev": true, + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz", + "integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==", + "dev": true + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz", + "integrity": "sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==", + "dev": true + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz", + "integrity": "sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==", + "dev": true + }, + "node_modules/@types/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==", + "dev": true + }, + "node_modules/@types/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", + "dev": true, + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==", + "dev": true + }, + "node_modules/@types/d3-selection": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.2.tgz", + "integrity": "sha512-d29EDd0iUBrRoKhPndhDY6U/PYxOWqgIZwKTooy2UkBfU7TNZNpRho0yLWPxlatQrFWk2mnTu71IZQ4+LRgKlQ==", + "dev": true + }, + "node_modules/@types/d3-shape": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.0.2.tgz", + "integrity": "sha512-5+ButCmIfNX8id5seZ7jKj3igdcxx+S9IDBiT35fQGTLZUfkFgTv+oBH34xgeoWDKpWcMITSzBILWQtBoN5Piw==", + "dev": true, + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", + "dev": true + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz", + "integrity": "sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==", + "dev": true + }, + "node_modules/@types/d3-timer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz", + "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==", + "dev": true + }, + "node_modules/@types/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-Sv4qEI9uq3bnZwlOANvYK853zvpdKEm1yz9rcc8ZTsxvRklcs9Fx4YFuGA3gXoQN/c/1T6QkVNjhaRO/cWj94g==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz", + "integrity": "sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ==", + "dev": true, + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/data-urls": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/data-urls/-/data-urls-3.0.4.tgz", + "integrity": "sha512-XRY2WVaOFSTKpNMaplqY1unPgAGk/DosOJ+eFrB6LJcFFbRH3nVbwJuGqLmDwdTWWx+V7U614/kmrj1JmCDl2A==", + "dev": true, + "dependencies": { + "@types/whatwg-mimetype": "*", + "@types/whatwg-url": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.2.tgz", + "integrity": "sha512-nQxgB8/Sg+QKhnV8e0WzPpxjIGT3tuJDDzybkDi8ItE/IgTlHo07U0shaIjzhcvQxlq9SDRE42lsJ23uvEgJ2A==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz", + "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.35", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", + "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/filesystem": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz", + "integrity": "sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==", + "dev": true, + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.29.tgz", + "integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==", + "dev": true + }, + "node_modules/@types/geojson": { + "version": "7946.0.8", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz", + "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==", + "dev": true + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/har-format": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.8.tgz", + "integrity": "sha512-OP6L9VuZNdskgNN3zFQQ54ceYD8OLq5IbqO4VK91ORLfOm7WdT/CiT/pHEBSQEqCInJ2y3O6iCm/zGtPElpgJQ==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.11", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz", + "integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.1.tgz", + "integrity": "sha512-Vu8l+UGcshYmV1VWwULgnV/2RDbBaO6i2Ptx7nd//oJPIZGhoI1YLST4VKagD2Pq/Bc2/7zvtvhM7F3p4SN7kQ==", + "dev": true + }, + "node_modules/@types/jasminewd2": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/jasminewd2/-/jasminewd2-2.0.10.tgz", + "integrity": "sha512-J7mDz7ovjwjc+Y9rR9rY53hFWKATcIkrr9DwQWmOas4/pnIPJTXawnzjwpHm3RSxz/e3ZVUvQ7cRbd5UQLo10g==", + "dev": true, + "dependencies": { + "@types/jasmine": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true + }, + "node_modules/@types/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-zK4gSFMjgslsv5Lyvr3O1yCjgmnE4pr8jbG8qVn4QglMwtpvPCf4YT2Wma7Nk95OxUUJI8Z+kzdXohbM7mVpGw==", + "peer": true + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.1.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.5.tgz", + "integrity": "sha512-IvGD1CD/nego63ySR7vrAKEX3AJTcmrAN2kn+/sDNLi1Ff5kBzDeEdqWDplK+0HAEoLYej137Sk0cUU8OLOlMg==", + "dev": true + }, + "node_modules/@types/psl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/psl/-/psl-1.1.0.tgz", + "integrity": "sha512-HhZnoLAvI2koev3czVPzBNRYvdrzJGLjQbWZhqFmS9Q6a0yumc5qtfSahBGb5g+6qWvA8iiQktqGkwoIXa/BNQ==", + "dev": true + }, + "node_modules/@types/q": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", + "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "node_modules/@types/selenium-webdriver": { + "version": "3.0.19", + "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-3.0.19.tgz", + "integrity": "sha512-OFUilxQg+rWL2FMxtmIgCkUDlJB6pskkpvmew7yeXfzzsOBb5rc+y2+DjHm+r3r1ZPPcJefK3DveNSYWGiy68g==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", + "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", + "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", + "dev": true, + "dependencies": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.33", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", + "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, + "node_modules/@types/topojson-client": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/topojson-client/-/topojson-client-3.1.1.tgz", + "integrity": "sha512-E4/Z2Xg56kVLRzYWem/6uOKVcVNqqxEqlWM9qCG2tCV1BxuzvvXC02/ELoGJWgtKkQhfycBPlMFEuTFdA/YiTg==", + "dev": true, + "dependencies": { + "@types/geojson": "*", + "@types/topojson-specification": "*" + } + }, + "node_modules/@types/topojson-simplify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/topojson-simplify/-/topojson-simplify-3.0.1.tgz", + "integrity": "sha512-H7SS2X11Lo3iRT3e7R6jPTAazOoSLD0LKIGq1b+4m/76Md46JfeU3zVIhxfIX9FY7oiyEbXwGumjK1GUXwIIMA==", + "dev": true, + "dependencies": { + "@types/geojson": "*", + "@types/topojson-specification": "*" + } + }, + "node_modules/@types/topojson-specification": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/topojson-specification/-/topojson-specification-1.0.2.tgz", + "integrity": "sha512-SGc1NdX9g3UGDp6S+p+uyG+Z8CehS51sUJ9bejA25Xgn2kkAguILk6J9nxXK+0M/mbTBN7ypMA7+4HVLNMJ8ag==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/webextension-polyfill": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@types/webextension-polyfill/-/webextension-polyfill-0.8.3.tgz", + "integrity": "sha512-GN+Hjzy9mXjWoXKmaicTegv3FJ0WFZ3aYz77Wk8TMp1IY3vEzvzj1vnsa0ggV7vMI1i+PUxe4qqnIJKCzf9aTg==", + "dev": true + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "dev": true + }, + "node_modules/@types/webpack": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.0.tgz", + "integrity": "sha512-8cP0CzcxUiFuA9xGJkfeVpqmWTk9nx6CWwamRGCj95ph1SmlRRk9KlCZ6avhCbZd4L68LvYT6l1kpdEnQXrF8w==", + "dev": true, + "dependencies": { + "@types/node": "*", + "tapable": "^2.2.0", + "webpack": "^5" + } + }, + "node_modules/@types/webpack-sources": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", + "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + } + }, + "node_modules/@types/whatwg-encoding": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/whatwg-encoding/-/whatwg-encoding-2.0.3.tgz", + "integrity": "sha512-7TJfeaSFIWAKQ4ZynOb5zV3xzJQEEmL0U0j+uH7tnqfL97apXDTwMo0dB2uAWXAbr2dRRi5/eO9jV9dK/1GkiA==", + "dev": true + }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.4.tgz", + "integrity": "sha512-lXCmTWSHJvf0TRSO58nm978b8HJ/EdsSsEKLd3ODHFjo+3VGAyyTp4v50nWvwtzBxSMQrVOK7tcuN0zGPLICMw==", + "dev": true, + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", + "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.59.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.6.tgz", + "integrity": "sha512-sXtOgJNEuRU5RLwPUb1jxtToZbgvq3M6FPpY4QENxoOggK+UpTxUBpj6tD8+Qh2g46Pi9We87E+eHnUw8YcGsw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.59.6", + "@typescript-eslint/type-utils": "5.59.6", + "@typescript-eslint/utils": "5.59.6", + "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.59.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.6.tgz", + "integrity": "sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.59.6", + "@typescript-eslint/types": "5.59.6", + "@typescript-eslint/typescript-estree": "5.59.6", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.59.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.6.tgz", + "integrity": "sha512-gLbY3Le9Dxcb8KdpF0+SJr6EQ+hFGYFl6tVY8VxLPFDfUZC7BHFw+Vq7bM5lE9DwWPfx4vMWWTLGXgpc0mAYyQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.6", + "@typescript-eslint/visitor-keys": "5.59.6" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.59.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.6.tgz", + "integrity": "sha512-A4tms2Mp5yNvLDlySF+kAThV9VTBPCvGf0Rp8nl/eoDX9Okun8byTKoj3fJ52IJitjWOk0fKPNQhXEB++eNozQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.59.6", + "@typescript-eslint/utils": "5.59.6", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@typescript-eslint/type-utils/node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.59.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.6.tgz", + "integrity": "sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.59.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.6.tgz", + "integrity": "sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.6", + "@typescript-eslint/visitor-keys": "5.59.6", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.59.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.6.tgz", + "integrity": "sha512-vzaaD6EXbTS29cVH0JjXBdzMt6VBlv+hE31XktDRMX1j3462wZCJa7VzO2AxXEXcIl8GQqZPcOPuW/Z1tZVogg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.59.6", + "@typescript-eslint/types": "5.59.6", + "@typescript-eslint/typescript-estree": "5.59.6", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.59.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.6.tgz", + "integrity": "sha512-zEfbFLzB9ETcEJ4HZEEsCR9HHeNku5/Qw1jSS5McYJv5BR+ftYXwFFAH5Al+xkGaZEqowMwl7uoJjQb1YSPF8Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.6", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.1.tgz", + "integrity": "sha512-pcub+YbFtFhaGRTo1832FQHQSHvMrlb43974e2eS8EKleR3p1cDdkJFPci1UhwkEf1J9Bz+wKBSzqpKp7nNj2A==", + "dev": true, + "engines": { + "node": ">=14.6.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "node_modules/@yarnpkg/parsers": { + "version": "3.0.0-rc.43", + "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.43.tgz", + "integrity": "sha512-AhFF3mIDfA+jEwQv2WMHmiYhOvmdbh2qhUkDVQfiqzQtUwS4BgoWwom5NpSPg4Ix5vOul+w1690Bt21CkVLpgg==", + "dev": true, + "dependencies": { + "js-yaml": "^3.10.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.15.0" + } + }, + "node_modules/@zkochan/js-yaml": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz", + "integrity": "sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@zkochan/js-yaml/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/adm-zip": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", + "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz", + "integrity": "sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "depd": "^2.0.0", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true + }, + "node_modules/archiver": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz", + "integrity": "sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w==", + "dev": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.3", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.0.0", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "dev": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/argparse/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.14", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", + "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.21.5", + "caniuse-lite": "^1.0.30001464", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-loader": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz", + "integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==", + "dev": true, + "dependencies": { + "find-cache-dir": "^3.3.2", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/blocking-proxy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz", + "integrity": "sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "blocking-proxy": "built/lib/bin.js" + }, + "engines": { + "node": ">=6.9.x" + } + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/body-parser/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bonjour-service": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", + "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "dev": true, + "dependencies": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/browserstack": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.6.1.tgz", + "integrity": "sha512-GxtFjpIaKdbAyzHfFDKixKO8IBT7wR3NjbzrGc78nNs/Ciys9wU3/nBtsqsWv5nDSrdI5tz0peKuzCPuNXNUiw==", + "dev": true, + "dependencies": { + "https-proxy-agent": "^2.2.1" + } + }, + "node_modules/browserstack/node_modules/agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "dependencies": { + "es6-promisify": "^5.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/browserstack/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/browserstack/node_modules/https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "dev": true, + "dependencies": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", + "dev": true, + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.0.6.tgz", + "integrity": "sha512-ixcYmEBExFa/+ajIPjcwypxL97CjJyOsH9A/W+4qgEPIpJvKlC+HmVY8nkIck6n3PwUTdgq9c489niJGwl+5Cw==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.4.tgz", + "integrity": "sha512-fDboBse/sl1oXSLhIp0FcCJgzW9KmhC/q8ULTKC82zc+DL3TL7FNb8qlt5qqXN53MsKEUSIcb+7DLmEygOE5Yw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.0", + "minipass": "^5.0.0 || ^6.0.0", + "path-scurry": "^1.7.0" + }, + "bin": { + "glob": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.0.tgz", + "integrity": "sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001487", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001487.tgz", + "integrity": "sha512-83564Z3yWGqXsh2vaH/mhXfEM0wX+NlBCm1jYHOb97TrTWJEmPTccZgeLTPBUUb0PNVo+oomb7wkimZBIERClA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/clean-webpack-plugin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz", + "integrity": "sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w==", + "dev": true, + "dependencies": { + "del": "^4.1.1" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "webpack": ">=4.0.0 <6.0.0" + } + }, + "node_modules/clean-webpack-plugin/node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-webpack-plugin/node_modules/del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "dependencies": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clean-webpack-plugin/node_modules/del/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/clean-webpack-plugin/node_modules/globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "dev": true, + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-webpack-plugin/node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/clean-webpack-plugin/node_modules/is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "dependencies": { + "is-path-inside": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clean-webpack-plugin/node_modules/is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "dependencies": { + "path-is-inside": "^1.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clean-webpack-plugin/node_modules/p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/clean-webpack-plugin/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "optional": true, + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/compress-commons": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", + "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==", + "dev": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.30.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.30.2.tgz", + "integrity": "sha512-nriW1nuJjUgvkEjIot1Spwakz52V9YkYHZAQG6A1eCgC8AA1p0zngrQEP9R0+V6hji5XilWKG1Bd0YRppmGimA==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "optional": true, + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cosmiconfig": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", + "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", + "dev": true, + "dependencies": { + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.3.0.tgz", + "integrity": "sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=7", + "ts-node": ">=10", + "typescript": ">=3" + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz", + "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", + "dev": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/critters": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.16.tgz", + "integrity": "sha512-JwjgmO6i3y6RWtLYmXwO5jMd+maZt8Tnfu7VVISmEWyQqfLpB8soBswf8/2bu6SBXxtKA68Al3c+qIG1ApT68A==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^4.2.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "postcss": "^8.3.7", + "pretty-bytes": "^5.3.0" + } + }, + "node_modules/critters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/critters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/critters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/critters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/critters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/critters/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "node_modules/critters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz", + "integrity": "sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.19", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cuint": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", + "integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==", + "dev": true + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/cytoscape": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.25.0.tgz", + "integrity": "sha512-7MW3Iz57mCUo6JQCho6CmPBCbTlJr7LzyEtIkutG255HLVd4XuBg2I9BkTZLI/e4HoaOB/BiAzXuQybQ95+r9Q==", + "optional": true, + "dependencies": { + "heap": "^0.2.6", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "optional": true, + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "optional": true, + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "optional": true, + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "optional": true + }, + "node_modules/d3": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.4.tgz", + "integrity": "sha512-q2WHStdhiBtD8DMmhDPyJmXUxr6VWRngKyiJ5EfXMxPw+tqT6BhNjhJZ4w3BHsNm3QoVfZLY8Orq/qPFczwKRA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz", + "integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.0.tgz", + "integrity": "sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.0.1.tgz", + "integrity": "sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.1.tgz", + "integrity": "sha512-LtAIu54UctRmhGKllleflmHalttH3zkfSi4NlKrTAoFKjC+AFBJohsCAdgCBYQwH0F8hIOGY89X1pPqAchlMkA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz", + "integrity": "sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz", + "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.9.tgz", + "integrity": "sha512-rYR4QfVmy+sR44IBDvVtcAmOReGBvRCWDpO2QjYwqgh9yijw6eSHBqaPG/LIOEy7aBsniLvtMW6pg19qJhq60w==", + "optional": true, + "dependencies": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/date-format": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.11.tgz", + "integrity": "sha512-VS20KRyorrbMCQmpdl2hg5KaOUsda1RbnsJg461FfrcyCUg+pkd0b40BSW4niQyTheww4DBXQnS7HwSrKkipLw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==", + "optional": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-equal": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.1.tgz", + "integrity": "sha512-lKdkdV6EOGoVn65XaOsPdH4rMxTZOnmFyuIkMjM1i5HHCbfjC97dawgTAy0deYNfuqUqW+Q5VrVaQYtUpSd6yQ==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.0", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.0", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-equal/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "dependencies": { + "globby": "^5.0.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "rimraf": "^2.2.8" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del/node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del/node_modules/globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "dependencies": { + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "dependencies": { + "robust-predicates": "^3.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "optional": true + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", + "dev": true + }, + "node_modules/dns-packet": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.0.tgz", + "integrity": "sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz", + "integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==", + "optional": true + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "node_modules/ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.396", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.396.tgz", + "integrity": "sha512-pqKTdqp/c5vsrc0xUPYXTDBo9ixZuGY8es4ZOjjd6HD6bFYbu5QA09VoW3fkY4LF1T0zYk86lN6bZnNlBuOpdQ==" + }, + "node_modules/elkjs": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", + "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==", + "optional": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/emoji-toolkit": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-7.0.1.tgz", + "integrity": "sha512-l5aJyAhpC5s4mDuoVuqt4SzVjwIsIvakPh4ZGJJE4KWuWFCEHaXacQFkStVdD9zbRR+/BbRXob7u99o0lQFr8A==" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz", + "integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", + "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.0.tgz", + "integrity": "sha512-+DCows0XNwLDcUhbFJPdlQEVnT2zXlCv7hPxemTz86/O+B/hCQ+mb7ydkPKiflpVraqLPCAfu7lDy+hBXueojw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.22.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.5.tgz", + "integrity": "sha512-oW69R+4q2wG+Hc3KZePPZxOiisRIqfKBVo/HLx94QcJeWGU/8sZhCvc829rd1kS366vlJbzBfXf9yWwf0+Ko7w==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.1", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.0", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.5", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-get-iterator/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/es-module-lexer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", + "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", + "dev": true + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "dependencies": { + "es6-promise": "^4.0.3" + } + }, + "node_modules/esbuild": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.18.tgz", + "integrity": "sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.18", + "@esbuild/android-arm64": "0.17.18", + "@esbuild/android-x64": "0.17.18", + "@esbuild/darwin-arm64": "0.17.18", + "@esbuild/darwin-x64": "0.17.18", + "@esbuild/freebsd-arm64": "0.17.18", + "@esbuild/freebsd-x64": "0.17.18", + "@esbuild/linux-arm": "0.17.18", + "@esbuild/linux-arm64": "0.17.18", + "@esbuild/linux-ia32": "0.17.18", + "@esbuild/linux-loong64": "0.17.18", + "@esbuild/linux-mips64el": "0.17.18", + "@esbuild/linux-ppc64": "0.17.18", + "@esbuild/linux-riscv64": "0.17.18", + "@esbuild/linux-s390x": "0.17.18", + "@esbuild/linux-x64": "0.17.18", + "@esbuild/netbsd-x64": "0.17.18", + "@esbuild/openbsd-x64": "0.17.18", + "@esbuild/sunos-x64": "0.17.18", + "@esbuild/win32-arm64": "0.17.18", + "@esbuild/win32-ia32": "0.17.18", + "@esbuild/win32-x64": "0.17.18" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.17.18.tgz", + "integrity": "sha512-h4m5zVa+KaDuRFIbH9dokMwovvkIjTQJS7/Ry+0Z1paVuS9aIkso2vdA2GmwH9GSvGX6w71WveJ3PfkoLuWaRw==", + "dev": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz", + "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.3", + "@eslint/js": "8.40.0", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.5.2", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter-asyncresource": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", + "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/express/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/express/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz", + "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", + "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.2.tgz", + "integrity": "sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g==", + "dev": true, + "dependencies": { + "minipass": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuse.js": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz", + "integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "dev": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "13.1.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz", + "integrity": "sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "optional": true, + "dependencies": { + "delegate": "^3.1.2" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", + "dev": true + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dev": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/har-validator/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/har-validator/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true + }, + "node_modules/hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hdr-histogram-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", + "integrity": "sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==", + "dev": true, + "dependencies": { + "@assemblyscript/loader": "^0.10.1", + "base64-js": "^1.2.0", + "pako": "^1.0.3" + } + }, + "node_modules/hdr-histogram-percentiles-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", + "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", + "dev": true + }, + "node_modules/heap": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", + "optional": true + }, + "node_modules/hosted-git-info": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", + "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", + "dev": true, + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "dev": true + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.3.tgz", + "integrity": "sha512-C7FfFoTA+bI10qfeydT8aZbvr91vAEU+2W5BZUlzPec47oNb07SsOfwYrtxuvOYdUApPP/Qlh4DtAO51Ekk2QA==", + "dev": true, + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.0.tgz", + "integrity": "sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", + "dev": true + }, + "node_modules/immutable": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", + "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.0.0.tgz", + "integrity": "sha512-t0ikzf5qkSFqRl1e6ejKBe+Tk2bsQd8ivEkcisyGXsku2t8NvXZ1Y3RRz5vxrDgOrTBOi13CvGsVoI5wVpd7xg==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/injection-js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/injection-js/-/injection-js-2.4.0.tgz", + "integrity": "sha512-6jiJt0tCAo9zjHbcwLiPL+IuNe9SQ6a9g0PEzafThW3fOQi0mrmiJGBJvDD6tmhPh8cQHIQtCOrJuBfQME4kPA==", + "dev": true, + "dependencies": { + "tslib": "^2.0.0" + } + }, + "node_modules/inquirer": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", + "integrity": "sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "dev": true + }, + "node_modules/ipaddr.js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", + "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-builtin-module/node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true + }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "dev": true, + "dependencies": { + "is-path-inside": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "dependencies": { + "path-is-inside": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/isbinaryfile": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.8.tgz", + "integrity": "sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/jackspeak": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.2.0.tgz", + "integrity": "sha512-r5XBrqIJfwRIjRt/Xr5fv9Wh09qyhHfKnYddDlpM+ibRR20qrYActpCAgU6U+d53EOEjzkvxPMVHSlgR7leXrQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.8.6", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.6.tgz", + "integrity": "sha512-G43Ub9IYEFfu72sua6rzooi8V8Gz2lkfk48rW20vEWCGizeaEPlKB1Kh8JIA84yQbiAEfqlPmSpGgCKKxH3rDA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jasmine": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.8.0.tgz", + "integrity": "sha1-awicChFXax8W3xG4AUbZHU6Lij4=", + "dev": true, + "dependencies": { + "exit": "^0.1.2", + "glob": "^7.0.6", + "jasmine-core": "~2.8.0" + }, + "bin": { + "jasmine": "bin/jasmine.js" + } + }, + "node_modules/jasmine-core": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.0.0.tgz", + "integrity": "sha512-BJLxZlSVyWPN/oyaS1IIvIjChghI9/xWsLAIJqL9J5Fz47CN3JNr8Lmik3S2S7QS2RxclYjvSVSXP7IR35PAmg==", + "dev": true + }, + "node_modules/jasmine-spec-reporter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-7.0.0.tgz", + "integrity": "sha512-OtC7JRasiTcjsaCBPtMO0Tl8glCejM4J4/dNuOJdA8lBjz4PmWjYQ6pzb0uzpBNAWJMDudYuj9OdXJWqM2QTJg==", + "dev": true, + "dependencies": { + "colors": "1.4.0" + } + }, + "node_modules/jasmine/node_modules/jasmine-core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz", + "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=", + "dev": true + }, + "node_modules/jasminewd2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jasminewd2/-/jasminewd2-2.2.0.tgz", + "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=", + "dev": true, + "engines": { + "node": ">= 6.9.x" + } + }, + "node_modules/jest-worker": { + "version": "27.4.6", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.6.tgz", + "integrity": "sha512-gHWJF/6Xi5CTG5QCvROr6GcmpIqNYpDJyc8A1h/DyXqH1tD6SnRCM0d3U5msV31D2LB/U+E0M+W4oyvKV44oNw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", + "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-sdsl": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", + "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js-yaml-loader": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/js-yaml-loader/-/js-yaml-loader-1.2.2.tgz", + "integrity": "sha512-H+NeuNrG6uOs/WMjna2SjkaCw13rMWiT/D7l9+9x5n8aq88BDsh2sRmdfxckWPIHtViYHWRG6XiCKYvS1dfyLg==", + "dev": true, + "dependencies": { + "js-yaml": "^3.13.1", + "loader-utils": "^1.2.3", + "un-eval": "^1.2.0" + } + }, + "node_modules/js-yaml-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/js-yaml-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dev": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/karma": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.2.tgz", + "integrity": "sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.4.1", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/katex": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.0.tgz", + "integrity": "sha512-wPRB4iUPysfH97wTgG5/tRLYxmKVq6Q4jRAWRVOUxXB1dsiv4cvcNjqabHkrOvJHM1Bpk3WrgmllSO1vIvP24w==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "optional": true, + "dependencies": { + "commander": "^8.0.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", + "integrity": "sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==", + "optional": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/launch-editor": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", + "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.7.3" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "optional": true + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/less": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", + "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==", + "dev": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz", + "integrity": "sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==", + "dev": true, + "dependencies": { + "klona": "^2.0.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/loader-runner": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", + "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "optional": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log4js": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.6.0.tgz", + "integrity": "sha512-3v8R7fd45UB6THucSht6wN2/7AZEruQbXdjygPZcxt5TA/msO6si9CN5MefUuKXbYnJHTBnYcx4famwcyQd+sA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "date-format": "^4.0.11", + "debug": "^4.3.4", + "flatted": "^3.2.5", + "rfdc": "^1.3.0", + "streamroller": "^3.1.1" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/make-fetch-happen/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-fetch-happen/node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.1.tgz", + "integrity": "sha512-UWbFJKvj5k+nETdteFndTpYxdeTMox/ULeqX5k/dpaQJCCFmj5EeKv3dBcyO2xmkRAx2vppRu5dVG7SOtsGOzA==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-9.4.3.tgz", + "integrity": "sha512-TLkQEtqhRSuEHSE34lh5bCa94KATCyluAXmFnNI2PRZwOpXFeqiJWwZl+d2CcemE1RS6QbbueSSq9QIg8Uxcyw==", + "optional": true, + "dependencies": { + "@braintree/sanitize-url": "^6.0.0", + "cytoscape": "^3.23.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.1.0", + "d3": "^7.4.0", + "dagre-d3-es": "7.0.9", + "dayjs": "^1.11.7", + "dompurify": "2.4.3", + "elkjs": "^0.8.2", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.2", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "dev": true, + "dependencies": { + "mime-db": "1.51.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.5.tgz", + "integrity": "sha512-9HaR++0mlgom81s95vvNjxkg52n2b5s//3ZTI1EtzFb98awsLSivs2LMsVqnQ3ay0PVhqWcGNyDaTE961FOcjQ==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-json-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "dev": true, + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/needle": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz", + "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==", + "dev": true, + "optional": true, + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "optional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/ng-packagr": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-16.0.1.tgz", + "integrity": "sha512-MiJvSR+8olzCViwkQ6ihHLFWVNLdsfUNPCxrZqR7u1nOC/dXlWPf//l2IG0KLdVhHNCiM64mNdwaTpgDEBMD3w==", + "dev": true, + "dependencies": { + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.0.0", + "ajv": "^8.11.0", + "ansi-colors": "^4.1.3", + "autoprefixer": "^10.4.12", + "browserslist": "^4.21.4", + "cacache": "^17.0.0", + "chokidar": "^3.5.3", + "commander": "^10.0.0", + "convert-source-map": "^2.0.0", + "dependency-graph": "^0.11.0", + "esbuild-wasm": "^0.17.0", + "fast-glob": "^3.2.12", + "find-cache-dir": "^3.3.2", + "injection-js": "^2.4.0", + "jsonc-parser": "^3.2.0", + "less": "^4.1.3", + "ora": "^5.1.0", + "piscina": "^3.2.0", + "postcss": "^8.4.16", + "postcss-url": "^10.1.3", + "rollup": "^3.0.0", + "rxjs": "^7.5.6", + "sass": "^1.55.0" + }, + "bin": { + "ng-packagr": "cli/main.js" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "optionalDependencies": { + "esbuild": "^0.17.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^16.0.0-next.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "tslib": "^2.3.0", + "typescript": ">=4.9.3 <5.1" + }, + "peerDependenciesMeta": { + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/ng-packagr/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/ng-packagr/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/ng-zorro-antd": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/ng-zorro-antd/-/ng-zorro-antd-16.1.0.tgz", + "integrity": "sha512-+KjXoA0+v/liTtVIHswmOAzB9UaGADrO1tL9AOZsTLq5sZM8+DmhtixGRoSMD8HkkhpMFhsgEIxoHlkxtn1SXg==", + "dependencies": { + "@angular/cdk": "^16.0.0", + "@ant-design/icons-angular": "^16.0.0", + "date-fns": "^2.16.1", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": "^16.0.0", + "@angular/common": "^16.0.0", + "@angular/core": "^16.0.0", + "@angular/forms": "^16.0.0", + "@angular/platform-browser": "^16.0.0", + "@angular/router": "^16.0.0" + } + }, + "node_modules/ngx-markdown": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/ngx-markdown/-/ngx-markdown-16.0.0.tgz", + "integrity": "sha512-/rlbXi+HBscJCDdwaTWIUrRkvwJicPnuAgeugOCZa0UbZ4VCWV3U0+uB1Zv6krRDF6FXJNXNLTUrMZV7yH8I6A==", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "clipboard": "^2.0.11", + "emoji-toolkit": "^7.0.0", + "katex": "^0.16.0", + "mermaid": "^9.1.2", + "prismjs": "^1.28.0" + }, + "peerDependencies": { + "@angular/common": "^16.0.0", + "@angular/core": "^16.0.0", + "@angular/platform-browser": "^16.0.0", + "@types/marked": "^4.3.0", + "marked": "^4.3.0", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.13.0" + } + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.3.1.tgz", + "integrity": "sha512-4Q16ZCqq3g8awk6UplT7AuxQ35XN4R/yf/+wSAwcBUAjg7l58RTactWaP8fIDTi0FzI7YcVLujwExakZlfWkXg==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/node-gyp-build": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz", + "integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==", + "dev": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==" + }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", + "optional": true + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", + "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", + "dev": true, + "dependencies": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.0.tgz", + "integrity": "sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.1.1.tgz", + "integrity": "sha512-dH3GmQL4vsPtld59cOn8uY0iOqRmqKvV+DLGwNXV/Q7MDgD2QfOADWd/mFXcIE5LVhYYGjA3baz6W9JneqnuCw==", + "dev": true, + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz", + "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-7.0.4.tgz", + "integrity": "sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==", + "dev": true, + "dependencies": { + "ignore-walk": "^6.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.1.tgz", + "integrity": "sha512-mRtvlBjTsJvfCCdmPtiu2bdlx8d/KXtF7yNXNWe7G0Z36qWA9Ny5zXsI2PfBZEv7SXgoxTmNaTzGSbbzDZChoA==", + "dev": true, + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz", + "integrity": "sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA==", + "dev": true, + "dependencies": { + "make-fetch-happen": "^11.0.0", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^10.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/npm-registry-fetch/node_modules/make-fetch-happen": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", + "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", + "dev": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass-fetch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.3.tgz", + "integrity": "sha512-n5ITsTkDqYkYJZjcRWzZt9qnZKCT7nKCosJhHoj7S7zD+BP4jVbWs+odsniw5TA3E0sLomhTKOKjF86wf11PuQ==", + "dev": true, + "dependencies": { + "minipass": "^5.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm-run-all/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/npm-run-all/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-all/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "dev": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nx": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/nx/-/nx-16.0.2.tgz", + "integrity": "sha512-8Z9Bo1D2VbYjyC/F2ONensKjm10snz1UfkzURZiFA+oXikBPldiH1u67TOTpoCYZfyYQg4l6h6EpOaAvHF6Abg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@nrwl/tao": "16.0.2", + "@parcel/watcher": "2.0.4", + "@yarnpkg/lockfile": "^1.1.0", + "@yarnpkg/parsers": "^3.0.0-rc.18", + "@zkochan/js-yaml": "0.0.6", + "axios": "^1.0.0", + "chalk": "^4.1.0", + "cli-cursor": "3.1.0", + "cli-spinners": "2.6.1", + "cliui": "^7.0.2", + "dotenv": "~10.0.0", + "enquirer": "~2.3.6", + "fast-glob": "3.2.7", + "figures": "3.2.0", + "flat": "^5.0.2", + "fs-extra": "^11.1.0", + "glob": "7.1.4", + "ignore": "^5.0.4", + "js-yaml": "4.1.0", + "jsonc-parser": "3.2.0", + "lines-and-columns": "~2.0.3", + "minimatch": "3.0.5", + "npm-run-path": "^4.0.1", + "open": "^8.4.0", + "semver": "7.3.4", + "string-width": "^4.2.3", + "strong-log-transformer": "^2.1.0", + "tar-stream": "~2.2.0", + "tmp": "~0.2.1", + "tsconfig-paths": "^4.1.2", + "tslib": "^2.3.0", + "v8-compile-cache": "2.3.0", + "yargs": "^17.6.2", + "yargs-parser": "21.1.1" + }, + "bin": { + "nx": "bin/nx.js" + }, + "optionalDependencies": { + "@nx/nx-darwin-arm64": "16.0.2", + "@nx/nx-darwin-x64": "16.0.2", + "@nx/nx-linux-arm-gnueabihf": "16.0.2", + "@nx/nx-linux-arm64-gnu": "16.0.2", + "@nx/nx-linux-arm64-musl": "16.0.2", + "@nx/nx-linux-x64-gnu": "16.0.2", + "@nx/nx-linux-x64-musl": "16.0.2", + "@nx/nx-win32-arm64-msvc": "16.0.2", + "@nx/nx-win32-x64-msvc": "16.0.2" + }, + "peerDependencies": { + "@swc-node/register": "^1.4.2", + "@swc/core": "^1.2.173" + }, + "peerDependenciesMeta": { + "@swc-node/register": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/nx/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/nx/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/nx/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/nx/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/nx/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/nx/node_modules/fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nx/node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/nx/node_modules/glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nx/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nx/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/nx/node_modules/lines-and-columns": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", + "integrity": "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/nx/node_modules/semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nx/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nx/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pacote": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.1.3.tgz", + "integrity": "sha512-aRts8cZqxiJVDitmAh+3z+FxuO3tLNWEmwDRPEpDDiZJaRz06clP4XX112ynMT5uF0QNoMPajBBHnaStUEPJXA==", + "dev": true, + "dependencies": { + "@npmcli/git": "^4.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^6.0.1", + "@npmcli/run-script": "^6.0.0", + "cacache": "^17.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^5.0.0", + "npm-package-arg": "^10.0.0", + "npm-packlist": "^7.0.0", + "npm-pick-manifest": "^8.0.0", + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^6.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^1.3.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "devOptional": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "devOptional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.9.1.tgz", + "integrity": "sha512-UgmoiySyjFxP6tscZDgWGEAgsW5ok8W3F5CJDnnH2pozwSTGE6eH7vwTotMwATWA2r5xqdkKdxYPkwlJjAI/3g==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1", + "minipass": "^5.0.0 || ^6.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.1.tgz", + "integrity": "sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/piscina": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-3.2.0.tgz", + "integrity": "sha512-yn/jMdHRw+q2ZJhFhyqsmANcbF6V2QwmD84c6xRau+QpQOmtrBCoRGdvTfeuFDYXB5W2m6MfLkjkvQa9lUSmIA==", + "dev": true, + "dependencies": { + "eventemitter-asyncresource": "^1.0.0", + "hdr-histogram-js": "^2.0.1", + "hdr-histogram-percentiles-obj": "^3.0.0" + }, + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", + "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^2.1.1" + }, + "engines": { + "node": ">= 14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-loader": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.0.tgz", + "integrity": "sha512-qLAFjvR2BFNz1H930P7mj1iuWJFjGey/nVhimfOAAQ1ZyPpcClAxP8+A55Sl8mBvM+K2a9Pjgdj10KpANWrNfw==", + "dev": true, + "dependencies": { + "cosmiconfig": "^8.1.3", + "jiti": "^1.18.2", + "klona": "^2.0.6", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-loader/node_modules/semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz", + "integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + } + ], + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.19" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-url": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-10.1.3.tgz", + "integrity": "sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw==", + "dev": true, + "dependencies": { + "make-dir": "~3.1.0", + "mime": "~2.5.2", + "minimatch": "~3.0.4", + "xxhashjs": "~0.2.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-url/node_modules/mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protractor": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/protractor/-/protractor-7.0.0.tgz", + "integrity": "sha512-UqkFjivi4GcvUQYzqGYNe0mLzfn5jiLmO8w9nMhQoJRLhy2grJonpga2IWhI6yJO30LibWXJJtA4MOIZD2GgZw==", + "dev": true, + "dependencies": { + "@types/q": "^0.0.32", + "@types/selenium-webdriver": "^3.0.0", + "blocking-proxy": "^1.0.0", + "browserstack": "^1.5.1", + "chalk": "^1.1.3", + "glob": "^7.0.3", + "jasmine": "2.8.0", + "jasminewd2": "^2.1.0", + "q": "1.4.1", + "saucelabs": "^1.5.0", + "selenium-webdriver": "3.6.0", + "source-map-support": "~0.4.0", + "webdriver-js-extender": "2.1.0", + "webdriver-manager": "^12.1.7", + "yargs": "^15.3.1" + }, + "bin": { + "protractor": "bin/protractor", + "webdriver-manager": "bin/webdriver-manager" + }, + "engines": { + "node": ">=10.13.x" + } + }, + "node_modules/protractor/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/protractor/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/protractor/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/protractor/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/protractor/node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/protractor/node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/protractor/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/protractor/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/protractor/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/protractor/node_modules/source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "dependencies": { + "source-map": "^0.5.6" + } + }, + "node_modules/protractor/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/protractor/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/protractor/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/protractor/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/protractor/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/protractor/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/protractor/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/protractor/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/protractor/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/purgecss": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-5.0.0.tgz", + "integrity": "sha512-RAnuxrGuVyLLTr8uMbKaxDRGWMgK5CCYDfRyUNNcaz5P3kGgD2b7ymQGYEyo2ST7Tl/ScwFgf5l3slKMxHSbrw==", + "dev": true, + "dependencies": { + "commander": "^9.0.0", + "glob": "^8.0.3", + "postcss": "^8.4.4", + "postcss-selector-parser": "^6.0.7" + }, + "bin": { + "purgecss": "bin/purgecss.js" + } + }, + "node_modules/purgecss/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/purgecss/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/purgecss/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/purgecss/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/q": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", + "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-package-json": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.3.tgz", + "integrity": "sha512-4QbpReW4kxFgeBQ0vPAqh2y8sXEB3D4t3jsXbJKIhBiF80KT6XRo45reqwtftju5J6ru1ax06A2Gb/wM1qCOEQ==", + "dev": true, + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", + "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/read-package-json/node_modules/glob": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.4.tgz", + "integrity": "sha512-fDboBse/sl1oXSLhIp0FcCJgzW9KmhC/q8ULTKC82zc+DL3TL7FNb8qlt5qqXN53MsKEUSIcb+7DLmEygOE5Yw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.0", + "minipass": "^5.0.0 || ^6.0.0", + "path-scurry": "^1.7.0" + }, + "bin": { + "glob": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/read-package-json/node_modules/json-parse-even-better-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", + "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/minimatch": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.0.tgz", + "integrity": "sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.2.tgz", + "integrity": "sha512-6RLVvwJtVwEDfPdn6X6Ille4/lxGl0ATOY4FN/B9nxQcgOazvvI0nodiD19ScKq0PvA/29VpaOQML36o5IzZWA==", + "dev": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "node_modules/regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.1.tgz", + "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==" + }, + "node_modules/rollup": { + "version": "3.21.8", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.8.tgz", + "integrity": "sha512-SSFV2T2fWtQ/vvBip85u2Nr0GNKireabH9d7nXswBg+XSH+jbVDSYptRAEbCEsquhs503rpPA9POYAp0/Jhasw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", + "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sass": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz", + "integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.2.2.tgz", + "integrity": "sha512-nrIdVAAte3B9icfBiGWvmMhT/D+eCDwnk+yA7VE/76dp/WkHX+i44Q/pfo71NYbwj0Ap+PGsn0ekOuU1WFJ2AA==", + "dev": true, + "dependencies": { + "klona": "^2.0.6", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/saucelabs": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.5.0.tgz", + "integrity": "sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==", + "dev": true, + "dependencies": { + "https-proxy-agent": "^2.2.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/saucelabs/node_modules/agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "dependencies": { + "es6-promisify": "^5.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/saucelabs/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/saucelabs/node_modules/https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "dev": true, + "dependencies": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "node_modules/schema-utils": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", + "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==", + "optional": true + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selenium-webdriver": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz", + "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==", + "dev": true, + "dependencies": { + "jszip": "^3.1.3", + "rimraf": "^2.5.4", + "tmp": "0.0.30", + "xml2js": "^0.4.17" + }, + "engines": { + "node": ">= 6.9.0" + } + }, + "node_modules/selenium-webdriver/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/selenium-webdriver/node_modules/tmp": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", + "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/selfsigned": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "dev": true, + "dependencies": { + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", + "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "node_modules/set-function-length": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sigstore": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-1.5.1.tgz", + "integrity": "sha512-FIPThk7S1oeFXn8O8yh7gpyiQb6lYXzMIlOBzXhId/f81VvU587xNCHc4jd2lZ9724UkKUYYTuKSYcjhDSRD/Q==", + "dev": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.1.0", + "make-fetch-happen": "^11.0.1", + "tuf-js": "^1.1.3" + }, + "bin": { + "sigstore": "bin/sigstore.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/sigstore/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/sigstore/node_modules/make-fetch-happen": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", + "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", + "dev": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/sigstore/node_modules/minipass-fetch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.3.tgz", + "integrity": "sha512-n5ITsTkDqYkYJZjcRWzZt9qnZKCT7nKCosJhHoj7S7zD+BP4jVbWs+odsniw5TA3E0sLomhTKOKjF86wf11PuQ==", + "dev": true, + "dependencies": { + "minipass": "^5.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/sirv": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", + "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^1.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", + "integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.4.1", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.3.tgz", + "integrity": "sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "dev": true, + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz", + "integrity": "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "dev": true + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sshpk": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ssri": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.4.tgz", + "integrity": "sha512-12+IR2CB2C28MMAw0Ncqwj5QbTcs0nGIhgJzYWzDkb21vWmfNI83KS4f3Ci6GI98WreIfG7o9UXp3C0qbpA8nQ==", + "dev": true, + "dependencies": { + "minipass": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/streamroller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.1.tgz", + "integrity": "sha512-iPhtd9unZ6zKdWgMeYGfSBuqCngyJy1B/GPi/lTpwGpa3bajuX30GjUVd0/Tn/Xhg0mr4DOSENozz9Y06qyonQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "date-format": "^4.0.10", + "debug": "^4.3.4", + "fs-extra": "^10.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.padend": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.5.tgz", + "integrity": "sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strong-log-transformer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz", + "integrity": "sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.1", + "minimist": "^1.2.0", + "through": "^2.3.4" + }, + "bin": { + "sl-log-transformer": "bin/sl-log-transformer.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "optional": true + }, + "node_modules/stylus": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz", + "integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@adobe/css-tools": "^4.0.1", + "debug": "^4.3.2", + "glob": "^7.1.6", + "sax": "~1.2.4", + "source-map": "^0.7.3" + }, + "bin": { + "stylus": "bin/stylus" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://opencollective.com/stylus" + } + }, + "node_modules/sucrase": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz", + "integrity": "sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tailwindcss": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz", + "integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.18.2", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.1.14", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.14.tgz", + "integrity": "sha512-piERznXu0U7/pW7cdSn7hjqySIVTYT6F76icmFk7ptU7dDYlXTm5r9A6K04R2vU3olYgoKeo1Cg3eeu5nhftAw==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/terser": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.1.tgz", + "integrity": "sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.8.tgz", + "integrity": "sha512-WiHL3ElchZMsK27P8uIUh4604IgJyAW47LVXGbEoB21DbQcZ+OuMpGjVYnEUaqcWM6dO8uS2qUbA7LSCWqvsbg==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/terser": { + "version": "5.17.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.3.tgz", + "integrity": "sha512-AudpAZKmZHkG9jueayypz4duuCFJMMNGRMwaPvQKWfxKedh8Z2x3OCoDqIIi1xx5+iwx1u6Au8XQcc9Lke65Yg==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "optional": true + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, + "node_modules/topojson-simplify": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/topojson-simplify/-/topojson-simplify-3.0.3.tgz", + "integrity": "sha512-V+pBjLVzSQ3+hSOxBiV01OVXgFiCmMO8ia3huxKEyIMTC1ApQHBcdXdOqcQ6U2JJJD31TZduwY6KyF15R8sUgg==", + "dependencies": { + "commander": "2", + "topojson-client": "3" + }, + "bin": { + "toposimplify": "bin/toposimplify" + } + }, + "node_modules/totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "optional": true, + "engines": { + "node": ">=6.10" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ts-node/node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "node_modules/tslint": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", + "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", + "deprecated": "TSLint has been deprecated in favor of ESLint. Please see https://github.com/palantir/tslint/issues/4534 for more information.", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.3", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.13.0", + "tsutils": "^2.29.0" + }, + "bin": { + "tslint": "bin/tslint" + }, + "engines": { + "node": ">=4.8.0" + }, + "peerDependencies": { + "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev" + } + }, + "node_modules/tslint/node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/tslint/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tslint/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "peerDependencies": { + "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tuf-js": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-1.1.6.tgz", + "integrity": "sha512-CXwFVIsXGbVY4vFiWF7TJKWmlKJAT8TWkH4RmiohJRcDJInix++F0dznDmoVbtJNzZ8yLprKUG4YrDIhv3nBMg==", + "dev": true, + "dependencies": { + "@tufjs/models": "1.0.4", + "debug": "^4.3.4", + "make-fetch-happen": "^11.1.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/tuf-js/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/tuf-js/node_modules/make-fetch-happen": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", + "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", + "dev": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/tuf-js/node_modules/minipass-fetch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.3.tgz", + "integrity": "sha512-n5ITsTkDqYkYJZjcRWzZt9qnZKCT7nKCosJhHoj7S7zD+BP4jVbWs+odsniw5TA3E0sLomhTKOKjF86wf11PuQ==", + "dev": true, + "dependencies": { + "minipass": "^5.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.5.tgz", + "integrity": "sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.35", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz", + "integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "optional": true, + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/un-eval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/un-eval/-/un-eval-1.2.0.tgz", + "integrity": "sha512-Wlj/pum6dQtGTPD/lclDtoVPkSfpjPfy1dwnnKw/sZP5DpBH9fLhBgQfsqNhe5/gS1D+vkZUuB771NRMUPA5CA==", + "dev": true + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/useragent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", + "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", + "dev": true, + "dependencies": { + "lru-cache": "4.1.x", + "tmp": "0.0.x" + } + }, + "node_modules/useragent/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/useragent/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", + "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", + "dev": true, + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/vite": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.1.tgz", + "integrity": "sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==", + "dev": true, + "dependencies": { + "esbuild": "^0.17.5", + "postcss": "^8.4.21", + "rollup": "^3.20.2" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-worker": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", + "integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==", + "optional": true + }, + "node_modules/webdriver-js-extender": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz", + "integrity": "sha512-lcUKrjbBfCK6MNsh7xaY2UAUmZwe+/ib03AjVOpFobX4O7+83BUveSrLfU0Qsyb1DaKJdQRbuU+kM9aZ6QUhiQ==", + "dev": true, + "dependencies": { + "@types/selenium-webdriver": "^3.0.0", + "selenium-webdriver": "^3.0.1" + }, + "engines": { + "node": ">=6.9.x" + } + }, + "node_modules/webdriver-manager": { + "version": "12.1.9", + "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.9.tgz", + "integrity": "sha512-Yl113uKm8z4m/KMUVWHq1Sjtla2uxEBtx2Ue3AmIlnlPAKloDn/Lvmy6pqWCUersVISpdMeVpAaGbNnvMuT2LQ==", + "dev": true, + "dependencies": { + "adm-zip": "^0.5.2", + "chalk": "^1.1.1", + "del": "^2.2.0", + "glob": "^7.0.3", + "ini": "^1.3.4", + "minimist": "^1.2.0", + "q": "^1.4.1", + "request": "^2.87.0", + "rimraf": "^2.5.2", + "semver": "^5.3.0", + "xml2js": "^0.4.17" + }, + "bin": { + "webdriver-manager": "bin/webdriver-manager" + }, + "engines": { + "node": ">=6.9.x" + } + }, + "node_modules/webdriver-manager/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webdriver-manager/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webdriver-manager/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webdriver-manager/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/webdriver-manager/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/webdriver-manager/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/webdriver-manager/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webdriver-manager/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/webextension-polyfill": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.8.0.tgz", + "integrity": "sha512-a19+DzlT6Kp9/UI+mF9XQopeZ+n2ussjhxHJ4/pmIGge9ijCDz7Gn93mNnjpZAk95T4Tae8iHZ6sSf869txqiQ==", + "dev": true + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack": { + "version": "5.80.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.80.0.tgz", + "integrity": "sha512-OIMiq37XK1rWO8mH9ssfFKZsXg4n6klTEDL7S8/HqbAOBBaiy8ABvXvz0dDCXeEF9gqwxSvVk611zFPjS8hJxA==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.13.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.8.0.tgz", + "integrity": "sha512-ZzoSBePshOKhr+hd8u6oCkZVwpVaXgpw23ScGLFpR6SjYI7+7iIWYarjN6OEYOfRt8o7ZyZZQk0DuMizJ+LEIg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "gzip-size": "^6.0.0", + "lodash": "^4.17.20", + "opener": "^1.5.2", + "sirv": "^1.0.7", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", + "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.0.2.tgz", + "integrity": "sha512-iOddiJzPcQC6lwOIu60vscbGWth8PCRcWRCwoQcTQf9RMoOWBHg5EyzpGdtSmGMrSPd5vHEfFXmVErQEmkRngQ==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.2.tgz", + "integrity": "sha512-5i6TrGBRxG4vnfDpB6qSQGfnB6skGBXNL5/542w2uRGLimX6qeE5BQMLrzIC3JYV/xlGOv+s+hTleI9AZKUQNw==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.1", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", + "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-ext-reloader": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/webpack-ext-reloader/-/webpack-ext-reloader-1.1.9.tgz", + "integrity": "sha512-6AVXGrjcVHKtIQn4yGGghJpiIV2h9F7hNKLsh1oP8m+d6H3QLF3jTNu3vNdKu/8Lab3J/gwb7Bm7tjZLa+DS6g==", + "dev": true, + "dependencies": { + "@types/webextension-polyfill": "^0.8.2", + "@types/webpack": "^5.28.0", + "@types/webpack-sources": "^3.2.0", + "clean-webpack-plugin": "^4.0.0", + "colors": "^1.4.0", + "cross-env": "^7.0.3", + "lodash": "^4.17.21", + "minimist": "^1.2.5", + "useragent": "^2.3.0", + "webextension-polyfill": "^0.8.0", + "webpack-sources": "^3.2.3", + "ws": "^8.4.2" + }, + "bin": { + "webpack-ext-reloader": "dist/webpack-ext-reloader-cli.js" + }, + "peerDependencies": { + "webpack": "^5.61.0" + } + }, + "node_modules/webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack/node_modules/acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", + "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "node_modules/which-typed-array": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz", + "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.6", + "call-bind": "^1.0.5", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xxhashjs": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz", + "integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==", + "dev": true, + "dependencies": { + "cuint": "^0.2.2" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yaml": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-a-folder": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/zip-a-folder/-/zip-a-folder-1.1.5.tgz", + "integrity": "sha512-w6I4mvWc6D0Q4pdzCSFbQih/ezYBdjwGZVbWRRFMOYcOdtE9TONZ7YtXCPnHj4XJQmXQxTOWcRGnPYxRn+d0mw==", + "dev": true, + "dependencies": { + "archiver": "^5.3.1" + } + }, + "node_modules/zip-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz", + "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==", + "dev": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.1.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zone.js": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.13.0.tgz", + "integrity": "sha512-7m3hNNyswsdoDobCkYNAy5WiUulkMd3+fWaGT9ij6iq3Zr/IwJo4RMCYPSDjT+r7tnPErmY9sZpKhWQ8S5k6XQ==", + "dependencies": { + "tslib": "^2.3.0" + } + } + }, + "dependencies": { + "@adobe/css-tools": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz", + "integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==", + "dev": true, + "optional": true, + "peer": true + }, + "@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true + }, + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@angular-builders/custom-webpack": { + "version": "16.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@angular-builders/custom-webpack/-/custom-webpack-16.0.0-beta.1.tgz", + "integrity": "sha512-C0tpgKJt++ciJ2nXtP2+fHOgzHUNyk5Su7bgTKY3yWMWlC9YfUMOlXHvNnCRUDaLqxXTsxQjGp56o9hPNd5miA==", + "dev": true, + "requires": { + "@angular-devkit/architect": ">=0.1600.0 < 0.1700.0", + "@angular-devkit/build-angular": "^16.0.0", + "@angular-devkit/core": "^16.0.0", + "lodash": "^4.17.15", + "ts-node": "^10.0.0", + "tsconfig-paths": "^4.1.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "@angular-devkit/build-angular": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.0.2.tgz", + "integrity": "sha512-jh6ez6k1tPmLTQ8J2T0CY+aRqLbhCvaExH6pqB7q6/bkDItcLPrybDGfJf05F0dHvZPB2fQEK0xYz9i92POofQ==", + "dev": true, + "requires": { + "@ampproject/remapping": "2.2.1", + "@angular-devkit/architect": "0.1600.2", + "@angular-devkit/build-webpack": "0.1600.2", + "@angular-devkit/core": "16.0.2", + "@babel/core": "7.21.4", + "@babel/generator": "7.21.4", + "@babel/helper-annotate-as-pure": "7.18.6", + "@babel/helper-split-export-declaration": "7.18.6", + "@babel/plugin-proposal-async-generator-functions": "7.20.7", + "@babel/plugin-transform-async-to-generator": "7.20.7", + "@babel/plugin-transform-runtime": "7.21.4", + "@babel/preset-env": "7.21.4", + "@babel/runtime": "7.21.0", + "@babel/template": "7.20.7", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "16.0.2", + "@vitejs/plugin-basic-ssl": "1.0.1", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.14", + "babel-loader": "9.1.2", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "4.21.5", + "cacache": "17.0.6", + "chokidar": "3.5.3", + "copy-webpack-plugin": "11.0.0", + "critters": "0.0.16", + "css-loader": "6.7.3", + "esbuild": "0.17.18", + "esbuild-wasm": "0.17.18", + "glob": "8.1.0", + "https-proxy-agent": "5.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "karma-source-map-support": "1.4.0", + "less": "4.1.3", + "less-loader": "11.1.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "magic-string": "0.30.0", + "mini-css-extract-plugin": "2.7.5", + "mrmime": "1.0.1", + "open": "8.4.2", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "2.3.1", + "piscina": "3.2.0", + "postcss": "8.4.23", + "postcss-loader": "7.2.4", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.62.1", + "sass-loader": "13.2.2", + "semver": "7.4.0", + "source-map-loader": "4.0.1", + "source-map-support": "0.5.21", + "terser": "5.17.1", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "tslib": "2.5.0", + "vite": "4.3.1", + "webpack": "5.80.0", + "webpack-dev-middleware": "6.0.2", + "webpack-dev-server": "4.13.2", + "webpack-merge": "5.8.0", + "webpack-subresource-integrity": "5.1.0" + } + }, + "@angular-devkit/build-webpack": { + "version": "0.1600.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1600.2.tgz", + "integrity": "sha512-B7EYoRMZOT3RcorxkXaHvMqwuNSttJCicZ99DmwBC41YlZOxpVVP6uM6wvYINGO0TMtu9bCmKkrSD8IC/hHetQ==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.1600.2", + "rxjs": "7.8.1" + } + }, + "@angular-devkit/core": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.0.2.tgz", + "integrity": "sha512-V4+t0BHO+QML9O2IiG2mJi8DtjeMOm4LAuG6tNDeiHZGAPOflvSPsKBtVl2JlXX/JxdLmyF4B6kRoAXRMKcwTg==", + "dev": true, + "requires": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "7.8.1", + "source-map": "0.7.4" + } + }, + "@ngtools/webpack": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.0.2.tgz", + "integrity": "sha512-8nPAOs2JLdMrAUf3sMkySzh66sPIkukO6HT8KVj726Dqm0Jtabjnxh0EI15Gkykj7HqH0Zw7/VyxpNQRfTA2UQ==", + "dev": true, + "requires": {} + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "postcss-loader": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.2.4.tgz", + "integrity": "sha512-F88rpxxNspo5hatIc+orYwZDtHFaVFOSIVAx+fBfJC1GmhWbVmPWtmg2gXKE1OxJbneOSGn8PWdIwsZFcruS+w==", + "dev": true, + "requires": { + "cosmiconfig": "^8.1.3", + "cosmiconfig-typescript-loader": "^4.3.0", + "klona": "^2.0.6", + "semver": "^7.3.8" + } + } + } + }, + "@angular-devkit/architect": { + "version": "0.1600.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1600.2.tgz", + "integrity": "sha512-2AOP3/dwLywcjkRr3ixR/lb0uBn1jzaMWwQR3o7ye3IuEA2sRtyWhUzsy6V7smKBKWPDIbXvX2TcqYZAJ87ccA==", + "dev": true, + "requires": { + "@angular-devkit/core": "16.0.2", + "rxjs": "7.8.1" + }, + "dependencies": { + "@angular-devkit/core": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.0.2.tgz", + "integrity": "sha512-V4+t0BHO+QML9O2IiG2mJi8DtjeMOm4LAuG6tNDeiHZGAPOflvSPsKBtVl2JlXX/JxdLmyF4B6kRoAXRMKcwTg==", + "dev": true, + "requires": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "7.8.1", + "source-map": "0.7.4" + } + } + } + }, + "@angular-devkit/build-angular": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.0.1.tgz", + "integrity": "sha512-VFhUViBfONOf6Ji4Lfkxlk+GN5l8Owm4Z0McqUIegrXsq3aSSStBBFdaDESpzhS6GIGqEBjjHMUQK8IlWT+EIQ==", + "dev": true, + "requires": { + "@ampproject/remapping": "2.2.1", + "@angular-devkit/architect": "0.1600.1", + "@angular-devkit/build-webpack": "0.1600.1", + "@angular-devkit/core": "16.0.1", + "@babel/core": "7.21.4", + "@babel/generator": "7.21.4", + "@babel/helper-annotate-as-pure": "7.18.6", + "@babel/helper-split-export-declaration": "7.18.6", + "@babel/plugin-proposal-async-generator-functions": "7.20.7", + "@babel/plugin-transform-async-to-generator": "7.20.7", + "@babel/plugin-transform-runtime": "7.21.4", + "@babel/preset-env": "7.21.4", + "@babel/runtime": "7.21.0", + "@babel/template": "7.20.7", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "16.0.1", + "@vitejs/plugin-basic-ssl": "1.0.1", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.14", + "babel-loader": "9.1.2", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "4.21.5", + "cacache": "17.0.6", + "chokidar": "3.5.3", + "copy-webpack-plugin": "11.0.0", + "critters": "0.0.16", + "css-loader": "6.7.3", + "esbuild": "0.17.18", + "esbuild-wasm": "0.17.18", + "glob": "8.1.0", + "https-proxy-agent": "5.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "karma-source-map-support": "1.4.0", + "less": "4.1.3", + "less-loader": "11.1.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "magic-string": "0.30.0", + "mini-css-extract-plugin": "2.7.5", + "mrmime": "1.0.1", + "open": "8.4.2", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "2.3.1", + "piscina": "3.2.0", + "postcss": "8.4.23", + "postcss-loader": "7.2.4", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.62.1", + "sass-loader": "13.2.2", + "semver": "7.4.0", + "source-map-loader": "4.0.1", + "source-map-support": "0.5.21", + "terser": "5.17.1", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "tslib": "2.5.0", + "vite": "4.3.1", + "webpack": "5.80.0", + "webpack-dev-middleware": "6.0.2", + "webpack-dev-server": "4.13.2", + "webpack-merge": "5.8.0", + "webpack-subresource-integrity": "5.1.0" + }, + "dependencies": { + "@angular-devkit/architect": { + "version": "0.1600.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1600.1.tgz", + "integrity": "sha512-7N3Dugrp3Fyyn3Q6RsxFNJJ2m1QuqcF3GHJcX7siINL37Hp6xI/q5gKffcd9rf20H1DYZE0VIbR1Sk31G6hMWg==", + "dev": true, + "requires": { + "@angular-devkit/core": "16.0.1", + "rxjs": "7.8.1" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "postcss-loader": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.2.4.tgz", + "integrity": "sha512-F88rpxxNspo5hatIc+orYwZDtHFaVFOSIVAx+fBfJC1GmhWbVmPWtmg2gXKE1OxJbneOSGn8PWdIwsZFcruS+w==", + "dev": true, + "requires": { + "cosmiconfig": "^8.1.3", + "cosmiconfig-typescript-loader": "^4.3.0", + "klona": "^2.0.6", + "semver": "^7.3.8" + } + } + } + }, + "@angular-devkit/build-webpack": { + "version": "0.1600.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1600.1.tgz", + "integrity": "sha512-yCy5A1UwGzpst3QJ/CRo2Y8HWRqTPOfwAPAVl91Lbch7gBFViRvq6E7N1XfQunPu/eXvKxbuq2mFSDqtyZ1mWw==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.1600.1", + "rxjs": "7.8.1" + }, + "dependencies": { + "@angular-devkit/architect": { + "version": "0.1600.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1600.1.tgz", + "integrity": "sha512-7N3Dugrp3Fyyn3Q6RsxFNJJ2m1QuqcF3GHJcX7siINL37Hp6xI/q5gKffcd9rf20H1DYZE0VIbR1Sk31G6hMWg==", + "dev": true, + "requires": { + "@angular-devkit/core": "16.0.1", + "rxjs": "7.8.1" + } + } + } + }, + "@angular-devkit/core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.0.1.tgz", + "integrity": "sha512-2uz98IqkKJlgnHbWQ7VeL4pb+snGAZXIama2KXi+k9GsRntdcw+udX8rL3G9SdUGUF+m6+147Y1oRBMHsO/v4w==", + "dev": true, + "requires": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "7.8.1", + "source-map": "0.7.4" + } + }, + "@angular-devkit/schematics": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.0.1.tgz", + "integrity": "sha512-A9D0LTYmiqiBa90GKcSuWb7hUouGIbm/AHbJbjL85WLLRbQA2PwKl7P5Mpd6nS/ZC0kfG4VQY3VOaDvb3qpI9g==", + "dev": true, + "requires": { + "@angular-devkit/core": "16.0.1", + "jsonc-parser": "3.2.0", + "magic-string": "0.30.0", + "ora": "5.4.1", + "rxjs": "7.8.1" + } + }, + "@angular-eslint/builder": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-16.0.1.tgz", + "integrity": "sha512-yjFltV+r3YjisVjASMPmWB/ASz39wdh0q5g0l6/4G+8yaxl6hEYs5o0ZOGeGdTFstCql8FGY+QKwKgsq9Ec4QQ==", + "dev": true, + "requires": { + "@nx/devkit": "16.0.2", + "nx": "16.0.2" + } + }, + "@angular-eslint/bundled-angular-compiler": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.0.1.tgz", + "integrity": "sha512-amvTgKHtZoygivW3LAYZ9qjLWsXM7/7eaRvaHdmAEdjyFnYQZ7UbWMPSQNz1mlW/AzTFvk9lGGQORglNOSDnww==", + "dev": true + }, + "@angular-eslint/eslint-plugin": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-16.0.1.tgz", + "integrity": "sha512-CM9keS9cH1QAfSVfsvhw/oGCZcP/D8gfekWwVNjN/uEMEAak0czn1KOG7JQkE36NXOGtwCpTspMi1aa9CVKo9g==", + "dev": true, + "requires": { + "@angular-eslint/utils": "16.0.1", + "@typescript-eslint/utils": "5.59.2" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", + "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2" + } + }, + "@typescript-eslint/types": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", + "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", + "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/utils": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", + "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", + "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "@angular-eslint/eslint-plugin-template": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-16.0.1.tgz", + "integrity": "sha512-1hyfs+Iq7K2x3mDDE4985d8vDcMyknbE9HKHKUtRLfLKC9gnV3N5d4+UeySQ7Rrjvgzkc1g9qHADyuhwRWpDSA==", + "dev": true, + "requires": { + "@angular-eslint/bundled-angular-compiler": "16.0.1", + "@angular-eslint/utils": "16.0.1", + "@typescript-eslint/type-utils": "5.59.2", + "@typescript-eslint/utils": "5.59.2", + "aria-query": "5.1.3", + "axobject-query": "3.1.1" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", + "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz", + "integrity": "sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "5.59.2", + "@typescript-eslint/utils": "5.59.2", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/types": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", + "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", + "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/utils": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", + "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", + "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "requires": { + "deep-equal": "^2.0.5" + } + }, + "axobject-query": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", + "integrity": "sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==", + "dev": true, + "requires": { + "deep-equal": "^2.0.5" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "@angular-eslint/schematics": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-16.0.1.tgz", + "integrity": "sha512-1oJJEWVbgPkNK1E8rAJfrgxzNWWzJKv3frTHeAm8gvZ7GftYhHjDcrcnxLWrYNxb9+q8Awi0hvGta/4HROmmnA==", + "dev": true, + "requires": { + "@angular-eslint/eslint-plugin": "16.0.1", + "@angular-eslint/eslint-plugin-template": "16.0.1", + "@nx/devkit": "16.0.2", + "ignore": "5.2.4", + "nx": "16.0.2", + "strip-json-comments": "3.1.1", + "tmp": "0.2.1" + }, + "dependencies": { + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + } + } + }, + "@angular-eslint/template-parser": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-16.0.1.tgz", + "integrity": "sha512-x0+SwSeqa3TiVZan6fE5grHsCkjGqU+zAS2DB6wAw5pyvgNAIjrI4cZEQ8pkgHfXe5tuumTKztlkpisah5s/hg==", + "dev": true, + "requires": { + "@angular-eslint/bundled-angular-compiler": "16.0.1", + "eslint-scope": "^7.0.0" + }, + "dependencies": { + "eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "@angular-eslint/utils": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-16.0.1.tgz", + "integrity": "sha512-2xnJuhIrMZEYK6UyBym6FaFXZgopIIbqfQ4sAtMWY6zYkCEsVUvx5qKIrsnXAwvpDQrv0WiMXteqi/5ICpVMZQ==", + "dev": true, + "requires": { + "@angular-eslint/bundled-angular-compiler": "16.0.1", + "@typescript-eslint/utils": "5.59.2" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", + "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2" + } + }, + "@typescript-eslint/types": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", + "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", + "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/utils": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", + "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", + "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "@angular/animations": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.0.1.tgz", + "integrity": "sha512-ziRq1hGJJuQqQUHqNpEMp9uy1pVutvL8oNvawblh32u4bnLsVQU5gMd6sTonn0x4sphEwMNnuEmp/q6QRIx+pA==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/cdk": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.0.1.tgz", + "integrity": "sha512-GupYss6x84RWEoy3JTYu4Igr2SxHuV6whVKMScQG2/Gm+winOsOn7YWm0IZQuFnjSWIF2Va5B0Tp0IjFHWxTvA==", + "requires": { + "parse5": "^7.1.2", + "tslib": "^2.3.0" + } + }, + "@angular/cli": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.0.1.tgz", + "integrity": "sha512-0vIAcq/S+3NXXN4/gBQFVGaxLUQ0zhRxxHQQuiT7GGII73UySuhwvaFB1BEhYG5HVJjRrP1F0ZYbvsvrmFzfXQ==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.1600.1", + "@angular-devkit/core": "16.0.1", + "@angular-devkit/schematics": "16.0.1", + "@schematics/angular": "16.0.1", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "4.0.0", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "npm-package-arg": "10.1.0", + "npm-pick-manifest": "8.0.1", + "open": "8.4.2", + "ora": "5.4.1", + "pacote": "15.1.3", + "resolve": "1.22.2", + "semver": "7.4.0", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "dependencies": { + "@angular-devkit/architect": { + "version": "0.1600.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1600.1.tgz", + "integrity": "sha512-7N3Dugrp3Fyyn3Q6RsxFNJJ2m1QuqcF3GHJcX7siINL37Hp6xI/q5gKffcd9rf20H1DYZE0VIbR1Sk31G6hMWg==", + "dev": true, + "requires": { + "@angular-devkit/core": "16.0.1", + "rxjs": "7.8.1" + } + } + } + }, + "@angular/common": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.0.1.tgz", + "integrity": "sha512-ic9Ri4Mepf4c0BTff7o4Oyl/a1vACNXXUzuoTwIjWnIqrH89dtwg7ncTD9Rv0N1lon7r4gXokTbn9A/Yk/0jbw==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/compiler": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.0.1.tgz", + "integrity": "sha512-7zNo6H1qVQow3T4EUul76SaIDSMRSl0hmtyWUzPjtWkxMjrCPSduqjA4/NHaG0KX1BsUvUtQEoDJ5jv/7EHWTQ==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/compiler-cli": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.0.1.tgz", + "integrity": "sha512-EW7Oxp8EuTz3vCNd4RAncZGB7dCUYviUkBA4PzuyPmL2copZPt12j9qx0pXXF3T6ydjoZ+99ZEgfkKOV6FeU3g==", + "requires": { + "@babel/core": "7.19.3", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.1.2", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "dependencies": { + "@babel/core": { + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", + "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.3", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.0", + "@babel/helpers": "^7.19.0", + "@babel/parser": "^7.19.3", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.3", + "@babel/types": "^7.19.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "@babel/generator": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz", + "integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==", + "requires": { + "@babel/types": "^7.21.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + } + } + }, + "@angular/core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.0.1.tgz", + "integrity": "sha512-3s4XBbzWgyWcjI0WFlNDKRxsbm4J+OKIL4mJCM9r8gWwno9y0K/giziAm9YMIJ4VOBIvrcMbOh85o44FCk8cRA==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/forms": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.0.1.tgz", + "integrity": "sha512-VbH/YnEBau0q97zI7BjSk0pu/i2S0Y/zmhvA2wgI2CCvtbqT6hCNdE/3rW6ZFBcnuCe+dFhuchXe6dX28epsvg==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/localize": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.0.1.tgz", + "integrity": "sha512-2zC7KE/JUA/JCHP+kEDSF8iZ9cyvd6OAPFE74yH8FjixQsaq9WhXiPtGkHC0bg9hWH858bRcCmA9BZr+zjntvA==", + "requires": { + "@babel/core": "7.19.3", + "glob": "8.1.0", + "yargs": "^17.2.1" + }, + "dependencies": { + "@babel/core": { + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", + "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.3", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.0", + "@babel/helpers": "^7.19.0", + "@babel/parser": "^7.19.3", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.3", + "@babel/types": "^7.19.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + } + }, + "@babel/generator": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz", + "integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==", + "requires": { + "@babel/types": "^7.21.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "@angular/platform-browser": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.0.1.tgz", + "integrity": "sha512-7XLIOnTnGDJLE4Q0zBz6eI9q5V3NnsTAJqIICJHc4gk6jNgVz90gtejAQ4EFbo0d83XGzwFL22hxID5Dj1WRIA==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/platform-browser-dynamic": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.0.1.tgz", + "integrity": "sha512-qrGlRPqJM42WZcHCbzwTA8SiK90xrhM/VrOL/8/1okuHn82gSWbbynpqycdZnsI9XMbW+HNhpKR2n8HKV38Jug==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/router": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.0.1.tgz", + "integrity": "sha512-4GH0SxPbuY08B/M0f3NEHf9yIFH+D3wlzWJHI75chfdqQ8gGAMG6B6PSmo3haicDxHcSnZTYNJXDLOQvaBAHcA==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@ant-design/colors": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.0.0.tgz", + "integrity": "sha512-iVm/9PfGCbC0dSMBrz7oiEXZaaGH7ceU40OJEfKmyuzR9R5CRimJYPlRiFtMQGQcbNMea/ePcoIebi4ASGYXtg==", + "requires": { + "@ctrl/tinycolor": "^3.4.0" + } + }, + "@ant-design/icons-angular": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/icons-angular/-/icons-angular-16.0.0.tgz", + "integrity": "sha512-KWBmWZl2so49R/MdAT7aG+xaBlMKl9SArR3Du/iPA0Am9GI1i9R89KgnnLWz+gkzHTye15S1IBXpgts4GPPU/w==", + "requires": { + "@ant-design/colors": "^7.0.0", + "tslib": "^2.0.0" + } + }, + "@assemblyscript/loader": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", + "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", + "dev": true + }, + "@babel/code-frame": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", + "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "requires": { + "@babel/highlight": "^7.18.6" + } + }, + "@babel/compat-data": { + "version": "7.21.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.7.tgz", + "integrity": "sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA==" + }, + "@babel/core": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz", + "integrity": "sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==", + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.21.4", + "@babel/helper-compilation-targets": "^7.21.4", + "@babel/helper-module-transforms": "^7.21.2", + "@babel/helpers": "^7.21.0", + "@babel/parser": "^7.21.4", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.4", + "@babel/types": "^7.21.4", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "@babel/generator": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz", + "integrity": "sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==", + "requires": { + "@babel/types": "^7.21.4", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.21.5.tgz", + "integrity": "sha512-uNrjKztPLkUk7bpCNC0jEKDJzzkvel/W+HguzbN8krA+LPfC1CEobJEvAvGka2A/M+ViOqXdcRL0GqPUJSjx9g==", + "dev": true, + "requires": { + "@babel/types": "^7.21.5" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz", + "integrity": "sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==", + "requires": { + "@babel/compat-data": "^7.21.5", + "@babel/helper-validator-option": "^7.21.0", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.21.8", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.8.tgz", + "integrity": "sha512-+THiN8MqiH2AczyuZrnrKL6cAxFRRQDKW9h1YkBvbgKmAm6mwiacig1qT73DHIWMGo40GRnsEfN3LA+E6NtmSw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.21.5", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-member-expression-to-functions": "^7.21.5", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.21.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/helper-split-export-declaration": "^7.18.6", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.21.8", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.8.tgz", + "integrity": "sha512-zGuSdedkFtsFHGbexAvNuipg1hbtitDLo2XE8/uf6Y9sOQV1xsYX/2pNbtedp/X0eU1pIt+kGvaqHCowkRbS5g==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "regexpu-core": "^5.3.1", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz", + "integrity": "sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==" + }, + "@babel/helper-function-name": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", + "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "requires": { + "@babel/template": "^7.20.7", + "@babel/types": "^7.21.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.5.tgz", + "integrity": "sha512-nIcGfgwpH2u4n9GG1HpStW5Ogx7x7ekiFHbjjFRKXbn5zUvqO9ZgotCO4x1aNbKn/x/xOUaXEhyNHCwtFCpxWg==", + "dev": true, + "requires": { + "@babel/types": "^7.21.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", + "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", + "requires": { + "@babel/types": "^7.21.4" + } + }, + "@babel/helper-module-transforms": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz", + "integrity": "sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw==", + "requires": { + "@babel/helper-environment-visitor": "^7.21.5", + "@babel/helper-module-imports": "^7.21.4", + "@babel/helper-simple-access": "^7.21.5", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.5", + "@babel/types": "^7.21.5" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz", + "integrity": "sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==", + "dev": true + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", + "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-wrap-function": "^7.18.9", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-replace-supers": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.21.5.tgz", + "integrity": "sha512-/y7vBgsr9Idu4M6MprbOVUfH3vs7tsIfnVWv/Ml2xgwvyH6LTngdfbf5AdsKwkJy4zgy1X/kuNrEKvhhK28Yrg==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.21.5", + "@babel/helper-member-expression-to-functions": "^7.21.5", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.5", + "@babel/types": "^7.21.5" + } + }, + "@babel/helper-simple-access": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz", + "integrity": "sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==", + "requires": { + "@babel/types": "^7.21.5" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "dev": true, + "requires": { + "@babel/types": "^7.20.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-string-parser": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", + "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==" + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + }, + "@babel/helper-validator-option": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", + "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==" + }, + "@babel/helper-wrap-function": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", + "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.19.0", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" + } + }, + "@babel/helpers": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.5.tgz", + "integrity": "sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==", + "requires": { + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.5", + "@babel/types": "^7.21.5" + } + }, + "@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.21.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.8.tgz", + "integrity": "sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==" + }, + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", + "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", + "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-proposal-optional-chaining": "^7.20.7" + } + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-class-static-block": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", + "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz", + "integrity": "sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-import-assertions": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", + "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.19.0" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.21.5.tgz", + "integrity": "sha512-wb1mhwGOCaXHDTcsRYMKF9e5bbMgqwxtqa2Y1ifH96dXJPwbuLX9qHy3clhrxVqgMz7nyNXs8VkxdH8UBcjKqA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.21.5" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", + "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", + "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz", + "integrity": "sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz", + "integrity": "sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-split-export-declaration": "^7.18.6", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.21.5.tgz", + "integrity": "sha512-TR653Ki3pAwxBxUe8srfF3e4Pe3FTA46uaNHYyQwIoM4oWKSoOZiDNyHJ0oIoDIUPSRQbQG7jzgVBX3FPVne1Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/template": "^7.20.7" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz", + "integrity": "sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", + "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", + "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", + "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.5.tgz", + "integrity": "sha512-nYWpjKW/7j/I/mZkGVgHJXh4bA1sfdFnJoOXwJuj4m3Q2EraO/8ZyrkCau9P5tbHQk01RMSt6KYLCsW7730SXQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.21.5" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", + "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", + "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", + "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", + "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.5.tgz", + "integrity": "sha512-OVryBEgKUbtqMoB7eG2rs6UFexJi6Zj6FDXx+esBLPTCxCNxAY9o+8Di7IsUGJ+AVhp5ncK0fxWUBd0/1gPhrQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.21.5", + "@babel/helper-plugin-utils": "^7.21.5", + "@babel/helper-simple-access": "^7.21.5" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", + "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-identifier": "^7.19.1" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", + "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", + "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", + "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", + "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz", + "integrity": "sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", + "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.21.5.tgz", + "integrity": "sha512-ZoYBKDb6LyMi5yCsByQ5jmXsHAQDDYeexT1Szvlmui+lADvfSecr5Dxd/PkrTC3pAD182Fcju1VQkB4oCp9M+w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.21.5", + "regenerator-transform": "^0.15.1" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", + "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-runtime": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.21.4.tgz", + "integrity": "sha512-1J4dhrw1h1PqnNNpzwxQ2UBymJUF8KuPjAAnlLwZcGhHAIqUigFW7cdK6GHoB64ubY4qXQNYknoUeks4Wz7CUA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.21.4", + "@babel/helper-plugin-utils": "^7.20.2", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", + "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", + "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", + "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", + "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", + "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.21.5.tgz", + "integrity": "sha512-LYm/gTOwZqsYohlvFUe/8Tujz75LqqVC2w+2qPHLR+WyWHGCZPN1KBpJCJn+4Bk4gOkQy/IXKIge6az5MqwlOg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.21.5" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", + "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/preset-env": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.21.4.tgz", + "integrity": "sha512-2W57zHs2yDLm6GD5ZpvNn71lZ0B/iypSdIeq25OurDKji6AdzV07qp4s3n1/x5BqtiGaTrPN3nerlSCaC5qNTw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.21.4", + "@babel/helper-compilation-targets": "^7.21.4", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.21.0", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.20.7", + "@babel/plugin-proposal-async-generator-functions": "^7.20.7", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.21.0", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.20.7", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.20.7", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.21.0", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.21.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.20.7", + "@babel/plugin-transform-async-to-generator": "^7.20.7", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.21.0", + "@babel/plugin-transform-classes": "^7.21.0", + "@babel/plugin-transform-computed-properties": "^7.20.7", + "@babel/plugin-transform-destructuring": "^7.21.3", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.21.0", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.20.11", + "@babel/plugin-transform-modules-commonjs": "^7.21.2", + "@babel/plugin-transform-modules-systemjs": "^7.20.11", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.20.5", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.21.3", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.20.5", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.20.7", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.21.4", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "@babel/runtime": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", + "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + } + }, + "@babel/traverse": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.5.tgz", + "integrity": "sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==", + "requires": { + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.21.5", + "@babel/helper-environment-visitor": "^7.21.5", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.21.5", + "@babel/types": "^7.21.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "@babel/generator": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz", + "integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==", + "requires": { + "@babel/types": "^7.21.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + } + } + }, + "@babel/types": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.5.tgz", + "integrity": "sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==", + "requires": { + "@babel/helper-string-parser": "^7.21.5", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + } + }, + "@braintree/sanitize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz", + "integrity": "sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==", + "optional": true + }, + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, + "@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==" + }, + "@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true + }, + "@esbuild/android-arm": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.18.tgz", + "integrity": "sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.18.tgz", + "integrity": "sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.18.tgz", + "integrity": "sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.18.tgz", + "integrity": "sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.18.tgz", + "integrity": "sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.18.tgz", + "integrity": "sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.18.tgz", + "integrity": "sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.18.tgz", + "integrity": "sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.18.tgz", + "integrity": "sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.18.tgz", + "integrity": "sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.18.tgz", + "integrity": "sha512-bvPG+MyFs5ZlwYclCG1D744oHk1Pv7j8psF5TfYx7otCVmcJsEXgFEhQkbhNW8otDHL1a2KDINW20cfCgnzgMQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.18.tgz", + "integrity": "sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.18.tgz", + "integrity": "sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.18.tgz", + "integrity": "sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.18.tgz", + "integrity": "sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.18.tgz", + "integrity": "sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.18.tgz", + "integrity": "sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.18.tgz", + "integrity": "sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.18.tgz", + "integrity": "sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.18.tgz", + "integrity": "sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.18.tgz", + "integrity": "sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.18.tgz", + "integrity": "sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==", + "dev": true, + "optional": true + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", + "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.2", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "@eslint/js": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", + "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", + "dev": true + }, + "@fortawesome/angular-fontawesome": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.13.0.tgz", + "integrity": "sha512-gzSPRdveOXNO7NIiMgTyB46aiHG0i98KinnAEqHXi8qzraM/kCcHn/0y3f4MhemX6kftwsFli0IU8RyHmtXlSQ==", + "requires": { + "tslib": "^2.4.1" + } + }, + "@fortawesome/fontawesome-common-types": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz", + "integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ==" + }, + "@fortawesome/fontawesome-svg-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.0.tgz", + "integrity": "sha512-Bertv8xOiVELz5raB2FlXDPKt+m94MQ3JgDfsVbrqNpLU9+UE2E18GKjLKw+d3XbeYPqg1pzyQKGsrzbw+pPaw==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.4.0" + } + }, + "@fortawesome/free-brands-svg-icons": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.0.tgz", + "integrity": "sha512-qvxTCo0FQ5k2N+VCXb/PZQ+QMhqRVM4OORiO6MXdG6bKolIojGU/srQ1ptvKk0JTbRgaJOfL2qMqGvBEZG7Z6g==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.4.0" + } + }, + "@fortawesome/free-regular-svg-icons": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.0.tgz", + "integrity": "sha512-ZfycI7D0KWPZtf7wtMFnQxs8qjBXArRzczABuMQqecA/nXohquJ5J/RCR77PmY5qGWkxAZDxpnUFVXKwtY/jPw==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.4.0" + } + }, + "@fortawesome/free-solid-svg-icons": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.0.tgz", + "integrity": "sha512-kutPeRGWm8V5dltFP1zGjQOEAzaLZj4StdQhWVZnfGFCvAPVvHh8qk5bRrU4KXnRRRNni5tKQI9PBAdI6MP8nQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.4.0" + } + }, + "@fullhuman/postcss-purgecss": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-5.0.0.tgz", + "integrity": "sha512-onDS/b/2pMRzqSoj4qOs2tYFmOpaspjTAgvACIHMPiicu1ptajiBruTrjBzTKdxWdX0ldaBb7wj8nEaTLyFkJw==", + "dev": true, + "requires": { + "purgecss": "^5.0.0" + } + }, + "@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" + }, + "@jridgewell/source-map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", + "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "dev": true + }, + "@ngtools/webpack": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.0.1.tgz", + "integrity": "sha512-CZHFPMiJuOe241kO1VSSPOQ5Z9hWWkY7eSs3hnS50Ntgd4YzlHAydqexmEFpXD2YLOFjdbNETCyJ2BQTM4Kwtw==", + "dev": true, + "requires": {} + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@npmcli/fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", + "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "dev": true, + "requires": { + "semver": "^7.3.5" + } + }, + "@npmcli/git": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.0.4.tgz", + "integrity": "sha512-5yZghx+u5M47LghaybLCkdSyFzV/w4OuH12d96HO389Ik9CDsLaDZJVynSGGVJOLn6gy/k7Dz5XYcplM3uxXRg==", + "dev": true, + "requires": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + }, + "which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "@npmcli/installed-package-contents": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.0.2.tgz", + "integrity": "sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ==", + "dev": true, + "requires": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + } + }, + "@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "dev": true, + "requires": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "dev": true + }, + "@npmcli/promise-spawn": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz", + "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==", + "dev": true, + "requires": { + "which": "^3.0.0" + }, + "dependencies": { + "which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "@npmcli/run-script": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-6.0.2.tgz", + "integrity": "sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==", + "dev": true, + "requires": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/promise-spawn": "^6.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^3.0.0", + "which": "^3.0.0" + }, + "dependencies": { + "which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "@nrwl/devkit": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-16.0.2.tgz", + "integrity": "sha512-SAEcImeQHdSTauO05FUn2vVl9/y5Kx1LNCZ4YE+SdY5/QRq18fuo/DCWmjOGG9M8r06vYGsAgMzkiB4soimcyA==", + "dev": true, + "requires": { + "@nx/devkit": "16.0.2" + } + }, + "@nrwl/tao": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-16.0.2.tgz", + "integrity": "sha512-wimEe4OTpI7/nDK67RnpZpEXCU+fzA0sDgpIhMgbpPd0vPmKgaZv4nbs8zrm0goFlacmmnLaGRhhGYMOxE+1Lg==", + "dev": true, + "requires": { + "nx": "16.0.2" + } + }, + "@nx/devkit": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-16.0.2.tgz", + "integrity": "sha512-BY1Bj0BbAl6XJL0O+QGTWPs/3WMJTEQ+Y4Lfoq4dZM7RllE6rAylr54NA2wa4lsgordZhq1+0g5PVhKKvSVRRw==", + "dev": true, + "requires": { + "@nrwl/devkit": "16.0.2", + "ejs": "^3.1.7", + "ignore": "^5.0.4", + "semver": "7.3.4", + "tmp": "~0.2.1", + "tslib": "^2.3.0" + }, + "dependencies": { + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + } + } + }, + "@nx/nx-darwin-arm64": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-16.0.2.tgz", + "integrity": "sha512-nAT8WJ/qKGEvUcoFLHHye1dbwCd7b8CTZJlDF+ZkyCD/UZRHt4eJxy8gvKmxgkZTFb2+PPMQt4UORCUGpZzuoA==", + "dev": true, + "optional": true + }, + "@nx/nx-darwin-x64": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-16.0.2.tgz", + "integrity": "sha512-r0rfOrZaOyrwFR5a0UT05xkYRumfkP65cRSZM1TjCA027AG9llYtkLT1hlz8uMKt+P12zrWVzXSqGLDi022ZZg==", + "dev": true, + "optional": true + }, + "@nx/nx-linux-arm-gnueabihf": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-16.0.2.tgz", + "integrity": "sha512-TfDQaGvCIDjn9sPg5U1Fr2rsSul/4PIQB59qrLBJRPiCWgpzwO71Il1qwSX68En+JH3lwXr+g5EjcDIEQ8fGYA==", + "dev": true, + "optional": true + }, + "@nx/nx-linux-arm64-gnu": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-16.0.2.tgz", + "integrity": "sha512-MICaUp7uz8WVQFXWPrmQaX1o4bdL7f3C7b3MDDf6+Zau6RcyQuw97UEKaYi9OqrV3w8yuPplqoLosFblAgb8uw==", + "dev": true, + "optional": true + }, + "@nx/nx-linux-arm64-musl": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-16.0.2.tgz", + "integrity": "sha512-wcBURG+6A2srm+6ujj8SShjwmYWs0eHI5D8vgZr8Bni+lXbKP/IosE9JGXKtRoh27/owyR8PGHhDVzjv46tlFg==", + "dev": true, + "optional": true + }, + "@nx/nx-linux-x64-gnu": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-16.0.2.tgz", + "integrity": "sha512-Xyml2gFdVDHUj2g67DKz2aD78x1BciN1ZaaBTCxXL4MHfwR78SZa7mtRtE+1kj5OgVIwupZP50jq7C8GuSn3Hw==", + "dev": true, + "optional": true + }, + "@nx/nx-linux-x64-musl": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-16.0.2.tgz", + "integrity": "sha512-j3xdN8I5DlTgW5N5eCquyBZswrrYf6EazUCvnEpeejygwh3N6XN7DlD68Bs0CB4Zmd0tWLfTjNVAtUJSP6g2mA==", + "dev": true, + "optional": true + }, + "@nx/nx-win32-arm64-msvc": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-16.0.2.tgz", + "integrity": "sha512-R2pzoW3SUFBbe9C1vifJnXuysPl6kmutQHN2yQ9lwJptzPvMxfDU1FuXmKCGRUGmEwFxk/XPhwDL/ZcbABTrzw==", + "dev": true, + "optional": true + }, + "@nx/nx-win32-x64-msvc": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-16.0.2.tgz", + "integrity": "sha512-r4H/SsqfpIJa8QLSpnscgkMnLsnkRYXj8TcILDrf+nJazfEdJZLUvVhN9O85OB7pskv86NuGfnJmJHHXy6QVQg==", + "dev": true, + "optional": true + }, + "@parcel/watcher": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.0.4.tgz", + "integrity": "sha512-cTDi+FUDBIUOBKEtj+nhiJ71AZVlkAsQFuGQTun5tV9mwQBQgZvhCzG+URPQc8myeN32yRVZEfVAPCs1RW+Jvg==", + "dev": true, + "requires": { + "node-addon-api": "^3.2.1", + "node-gyp-build": "^4.3.0" + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, + "@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, + "@rollup/plugin-json": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.0.0.tgz", + "integrity": "sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1" + } + }, + "@rollup/plugin-node-resolve": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.2.tgz", + "integrity": "sha512-Y35fRGUjC3FaurG722uhUuG8YHOJRJQbI6/CkbRkdPotSpDj9NtIN85z1zrcyDcCQIW4qp5mgG72U+gJ0TAFEg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.1", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + } + }, + "@rollup/pluginutils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", + "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + } + }, + "@schematics/angular": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.0.1.tgz", + "integrity": "sha512-MNgH/iB3WWxMLFVHJjtXCHZ8YHtfx2e3mX2Ds5P43OTgSnTk6tHabqvwxJ4wzjoyoPUyXWLhHt0diCmVtDTNeQ==", + "dev": true, + "requires": { + "@angular-devkit/core": "16.0.1", + "@angular-devkit/schematics": "16.0.1", + "jsonc-parser": "3.2.0" + } + }, + "@sigstore/protobuf-specs": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.1.0.tgz", + "integrity": "sha512-a31EnjuIDSX8IXBUib3cYLDRlPMU36AWX4xS8ysLaNu4ZzUesDiPt83pgrW2X1YLMe5L2HbDyaKK5BrL4cNKaQ==", + "dev": true + }, + "@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "dev": true, + "optional": true, + "peer": true + }, + "@tauri-apps/api": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-beta.3.tgz", + "integrity": "sha512-gDSJzKpBs6efXw2ZWqjl9QVNImY5GR5qygXqB7JK4y7prcQInxnTj2ARFR0vD4wuzkrUHGrlIKraiJJPHWJ9vg==" + }, + "@tauri-apps/plugin-cli": { + "version": "2.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-cli/-/plugin-cli-2.0.0-beta.1.tgz", + "integrity": "sha512-8VB0RTFi6SrCZvWDiOW+DVhCo7IsBenWfTIF6f8YAU+TnLSOAxpVc2MOM5PimVdKU2hu+mlpjSmPhd9RSCRfAw==", + "requires": { + "@tauri-apps/api": "2.0.0-beta.2" + }, + "dependencies": { + "@tauri-apps/api": { + "version": "2.0.0-beta.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-beta.2.tgz", + "integrity": "sha512-4r1r6kgttzIWxJ3HxkZQH+b7EiUtKhdUCPbi0KSalD+2T3j6klw+v8VyxhKwEdjM/eo60NE+J33v1E/Urq8puw==" + } + } + }, + "@tauri-apps/plugin-clipboard-manager": { + "version": "2.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0-alpha.4.tgz", + "integrity": "sha512-/xPQBXuzD8cSh81xkTphIAKxSD2kGsv8deKK+Qoh+89puay1xJjjnxVv5b9IKKn0G8r8HPm+JDEamlKxQbOgnA==", + "requires": { + "@tauri-apps/api": "2.0.0-alpha.12" + }, + "dependencies": { + "@tauri-apps/api": { + "version": "2.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-alpha.12.tgz", + "integrity": "sha512-acpNZQxFgHMHC5qV/IUg4IL/xmypzfxHB4ECkwb58fT48H4zBmklNd5TC0k7BvLUBoSmmgHc4InbYwQai392Yw==" + } + } + }, + "@tauri-apps/plugin-dialog": { + "version": "2.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0-alpha.4.tgz", + "integrity": "sha512-4NxBgDzxrZ8hPE9OMRYwsXYN2BxQYI/5l1UKEI5V4srFTZK81Vj5GGksCf7gQREZg7CmBRCk95qYx338A6oCag==", + "requires": { + "@tauri-apps/api": "2.0.0-alpha.12" + }, + "dependencies": { + "@tauri-apps/api": { + "version": "2.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-alpha.12.tgz", + "integrity": "sha512-acpNZQxFgHMHC5qV/IUg4IL/xmypzfxHB4ECkwb58fT48H4zBmklNd5TC0k7BvLUBoSmmgHc4InbYwQai392Yw==" + } + } + }, + "@tauri-apps/plugin-notification": { + "version": "2.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.0.0-alpha.4.tgz", + "integrity": "sha512-mXUuZoZEEMAedGNJxPZPLET3vY4lSmHCpfrfZIytJRU6eSxbec90L3fB4YqvW9+yqkplyXkvpiThILbT5A4Q4w==", + "requires": { + "@tauri-apps/api": "2.0.0-alpha.12" + }, + "dependencies": { + "@tauri-apps/api": { + "version": "2.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-alpha.12.tgz", + "integrity": "sha512-acpNZQxFgHMHC5qV/IUg4IL/xmypzfxHB4ECkwb58fT48H4zBmklNd5TC0k7BvLUBoSmmgHc4InbYwQai392Yw==" + } + } + }, + "@tauri-apps/plugin-os": { + "version": "2.0.0-alpha.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0-alpha.5.tgz", + "integrity": "sha512-dedPdad+ykMSZz2KUfrhUDyy32G2WH5aLkYdcACF58KC6GBvKuyR5sQ1ZE/pddo2L6VRhyujLp8zJEfRN3AUcQ==", + "requires": { + "@tauri-apps/api": "2.0.0-alpha.12" + }, + "dependencies": { + "@tauri-apps/api": { + "version": "2.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-alpha.12.tgz", + "integrity": "sha512-acpNZQxFgHMHC5qV/IUg4IL/xmypzfxHB4ECkwb58fT48H4zBmklNd5TC0k7BvLUBoSmmgHc4InbYwQai392Yw==" + } + } + }, + "@tauri-apps/plugin-shell": { + "version": "2.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-alpha.4.tgz", + "integrity": "sha512-Go/+EwGVuAXbSg2l2M5E2gT6cir66KV4CXC9P4gPHeead8Ar/B9wQvuINzcrYzL/HCcL7fFfKlqqu/XPTN2qvQ==", + "requires": { + "@tauri-apps/api": "2.0.0-alpha.12" + }, + "dependencies": { + "@tauri-apps/api": { + "version": "2.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-alpha.12.tgz", + "integrity": "sha512-acpNZQxFgHMHC5qV/IUg4IL/xmypzfxHB4ECkwb58fT48H4zBmklNd5TC0k7BvLUBoSmmgHc4InbYwQai392Yw==" + } + } + }, + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true + }, + "@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, + "@tufjs/canonical-json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz", + "integrity": "sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ==", + "dev": true + }, + "@tufjs/models": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-1.0.4.tgz", + "integrity": "sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A==", + "dev": true, + "requires": { + "@tufjs/canonical-json": "1.0.0", + "minimatch": "^9.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.0.tgz", + "integrity": "sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/bonjour": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/chrome": { + "version": "0.0.236", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.236.tgz", + "integrity": "sha512-ArQoxO9WtDY6GWcT2cpo+D+hyASPeFt7PHQEUDXwQhRS00Rbop07rnEOA046yws0HkM83Tcew/hW6Dgvnj4iMQ==", + "dev": true, + "requires": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/connect-history-api-fallback": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", + "integrity": "sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true, + "optional": true, + "peer": true + }, + "@types/cors": { + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@types/node": "*" + } + }, + "@types/d3": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.0.tgz", + "integrity": "sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==", + "dev": true, + "requires": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "@types/d3-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.2.tgz", + "integrity": "sha512-5mjGjz6XOXKOCdTajXTZ/pMsg236RdiwKPrRPWAEf/2S/+PzwY+LLYShUpeysWaMvsdS7LArh6GdUefoxpchsQ==", + "dev": true + }, + "@types/d3-axis": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.1.tgz", + "integrity": "sha512-zji/iIbdd49g9WN0aIsGcwcTBUkgLsCSwB+uH+LPVDAiKWENMtI3cJEWt+7/YYwelMoZmbBfzA3qCdrZ2XFNnw==", + "dev": true, + "requires": { + "@types/d3-selection": "*" + } + }, + "@types/d3-brush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.1.tgz", + "integrity": "sha512-B532DozsiTuQMHu2YChdZU0qsFJSio3Q6jmBYGYNp3gMDzBmuFFgPt9qKA4VYuLZMp4qc6eX7IUFUEsvHiXZAw==", + "dev": true, + "requires": { + "@types/d3-selection": "*" + } + }, + "@types/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-eQfcxIHrg7V++W8Qxn6QkqBNBokyhdWSAS73AbkbMzvLQmVVBviknoz2SRS/ZJdIOmhcmmdCRE/NFOm28Z1AMw==", + "dev": true + }, + "@types/d3-color": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.0.2.tgz", + "integrity": "sha512-WVx6zBiz4sWlboCy7TCgjeyHpNjMsoF36yaagny1uXfbadc9f+5BeBf7U+lRmQqY3EHbGQpP8UdW8AC+cywSwQ==", + "dev": true + }, + "@types/d3-contour": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.1.tgz", + "integrity": "sha512-C3zfBrhHZvrpAAK3YXqLWVAGo87A4SvJ83Q/zVJ8rFWJdKejUnDYaWZPkA8K84kb2vDA/g90LTQAz7etXcgoQQ==", + "dev": true, + "requires": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "@types/d3-delaunay": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.0.tgz", + "integrity": "sha512-iGm7ZaGLq11RK3e69VeMM6Oqj2SjKUB9Qhcyd1zIcqn2uE8w9GFB445yCY46NOQO3ByaNyktX1DK+Etz7ZaX+w==", + "dev": true + }, + "@types/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-NhxMn3bAkqhjoxabVJWKryhnZXXYYVQxaBnbANu0O94+O/nX9qSjrA1P1jbAQJxJf+VC72TxDX/YJcKue5bRqw==", + "dev": true + }, + "@types/d3-drag": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.1.tgz", + "integrity": "sha512-o1Va7bLwwk6h03+nSM8dpaGEYnoIG19P0lKqlic8Un36ymh9NSkNFX1yiXMKNMx8rJ0Kfnn2eovuFaL6Jvj0zA==", + "dev": true, + "requires": { + "@types/d3-selection": "*" + } + }, + "@types/d3-dsv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.0.tgz", + "integrity": "sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A==", + "dev": true + }, + "@types/d3-ease": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz", + "integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==", + "dev": true + }, + "@types/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-toZJNOwrOIqz7Oh6Q7l2zkaNfXkfR7mFSJvGvlD/Ciq/+SQ39d5gynHJZ/0fjt83ec3WL7+u3ssqIijQtBISsw==", + "dev": true, + "requires": { + "@types/d3-dsv": "*" + } + }, + "@types/d3-force": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.3.tgz", + "integrity": "sha512-z8GteGVfkWJMKsx6hwC3SiTSLspL98VNpmvLpEFJQpZPq6xpA1I8HNBDNSpukfK0Vb0l64zGFhzunLgEAcBWSA==", + "dev": true + }, + "@types/d3-format": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", + "dev": true + }, + "@types/d3-geo": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.2.tgz", + "integrity": "sha512-DbqK7MLYA8LpyHQfv6Klz0426bQEf7bRTvhMy44sNGVyZoWn//B0c+Qbeg8Osi2Obdc9BLLXYAKpyWege2/7LQ==", + "dev": true, + "requires": { + "@types/geojson": "*" + } + }, + "@types/d3-hierarchy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.0.2.tgz", + "integrity": "sha512-+krnrWOZ+aQB6v+E+jEkmkAx9HvsNAD+1LCD0vlBY3t+HwjKnsBFbpVLx6WWzDzCIuiTWdAxXMEnGnVXpB09qQ==", + "dev": true + }, + "@types/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "dev": true, + "requires": { + "@types/d3-color": "*" + } + }, + "@types/d3-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz", + "integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==", + "dev": true + }, + "@types/d3-polygon": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz", + "integrity": "sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==", + "dev": true + }, + "@types/d3-quadtree": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz", + "integrity": "sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==", + "dev": true + }, + "@types/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==", + "dev": true + }, + "@types/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", + "dev": true, + "requires": { + "@types/d3-time": "*" + } + }, + "@types/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==", + "dev": true + }, + "@types/d3-selection": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.2.tgz", + "integrity": "sha512-d29EDd0iUBrRoKhPndhDY6U/PYxOWqgIZwKTooy2UkBfU7TNZNpRho0yLWPxlatQrFWk2mnTu71IZQ4+LRgKlQ==", + "dev": true + }, + "@types/d3-shape": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.0.2.tgz", + "integrity": "sha512-5+ButCmIfNX8id5seZ7jKj3igdcxx+S9IDBiT35fQGTLZUfkFgTv+oBH34xgeoWDKpWcMITSzBILWQtBoN5Piw==", + "dev": true, + "requires": { + "@types/d3-path": "*" + } + }, + "@types/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", + "dev": true + }, + "@types/d3-time-format": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz", + "integrity": "sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==", + "dev": true + }, + "@types/d3-timer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz", + "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==", + "dev": true + }, + "@types/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-Sv4qEI9uq3bnZwlOANvYK853zvpdKEm1yz9rcc8ZTsxvRklcs9Fx4YFuGA3gXoQN/c/1T6QkVNjhaRO/cWj94g==", + "dev": true, + "requires": { + "@types/d3-selection": "*" + } + }, + "@types/d3-zoom": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz", + "integrity": "sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ==", + "dev": true, + "requires": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "@types/data-urls": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/data-urls/-/data-urls-3.0.4.tgz", + "integrity": "sha512-XRY2WVaOFSTKpNMaplqY1unPgAGk/DosOJ+eFrB6LJcFFbRH3nVbwJuGqLmDwdTWWx+V7U614/kmrj1JmCDl2A==", + "dev": true, + "requires": { + "@types/whatwg-mimetype": "*", + "@types/whatwg-url": "*" + } + }, + "@types/eslint": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.2.tgz", + "integrity": "sha512-nQxgB8/Sg+QKhnV8e0WzPpxjIGT3tuJDDzybkDi8ItE/IgTlHo07U0shaIjzhcvQxlq9SDRE42lsJ23uvEgJ2A==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz", + "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "@types/express": { + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.35", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", + "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "@types/filesystem": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz", + "integrity": "sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==", + "dev": true, + "requires": { + "@types/filewriter": "*" + } + }, + "@types/filewriter": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.29.tgz", + "integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==", + "dev": true + }, + "@types/geojson": { + "version": "7946.0.8", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz", + "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==", + "dev": true + }, + "@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/har-format": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.8.tgz", + "integrity": "sha512-OP6L9VuZNdskgNN3zFQQ54ceYD8OLq5IbqO4VK91ORLfOm7WdT/CiT/pHEBSQEqCInJ2y3O6iCm/zGtPElpgJQ==", + "dev": true + }, + "@types/http-proxy": { + "version": "1.17.11", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz", + "integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/jasmine": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.1.tgz", + "integrity": "sha512-Vu8l+UGcshYmV1VWwULgnV/2RDbBaO6i2Ptx7nd//oJPIZGhoI1YLST4VKagD2Pq/Bc2/7zvtvhM7F3p4SN7kQ==", + "dev": true + }, + "@types/jasminewd2": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/jasminewd2/-/jasminewd2-2.0.10.tgz", + "integrity": "sha512-J7mDz7ovjwjc+Y9rR9rY53hFWKATcIkrr9DwQWmOas4/pnIPJTXawnzjwpHm3RSxz/e3ZVUvQ7cRbd5UQLo10g==", + "dev": true, + "requires": { + "@types/jasmine": "*" + } + }, + "@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true + }, + "@types/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-zK4gSFMjgslsv5Lyvr3O1yCjgmnE4pr8jbG8qVn4QglMwtpvPCf4YT2Wma7Nk95OxUUJI8Z+kzdXohbM7mVpGw==", + "peer": true + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "@types/node": { + "version": "20.1.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.5.tgz", + "integrity": "sha512-IvGD1CD/nego63ySR7vrAKEX3AJTcmrAN2kn+/sDNLi1Ff5kBzDeEdqWDplK+0HAEoLYej137Sk0cUU8OLOlMg==", + "dev": true + }, + "@types/psl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/psl/-/psl-1.1.0.tgz", + "integrity": "sha512-HhZnoLAvI2koev3czVPzBNRYvdrzJGLjQbWZhqFmS9Q6a0yumc5qtfSahBGb5g+6qWvA8iiQktqGkwoIXa/BNQ==", + "dev": true + }, + "@types/q": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", + "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=", + "dev": true + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, + "@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "@types/selenium-webdriver": { + "version": "3.0.19", + "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-3.0.19.tgz", + "integrity": "sha512-OFUilxQg+rWL2FMxtmIgCkUDlJB6pskkpvmew7yeXfzzsOBb5rc+y2+DjHm+r3r1ZPPcJefK3DveNSYWGiy68g==", + "dev": true + }, + "@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "dev": true + }, + "@types/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", + "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "dev": true, + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/serve-static": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", + "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", + "dev": true, + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "@types/sockjs": { + "version": "0.3.33", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", + "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, + "@types/topojson-client": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/topojson-client/-/topojson-client-3.1.1.tgz", + "integrity": "sha512-E4/Z2Xg56kVLRzYWem/6uOKVcVNqqxEqlWM9qCG2tCV1BxuzvvXC02/ELoGJWgtKkQhfycBPlMFEuTFdA/YiTg==", + "dev": true, + "requires": { + "@types/geojson": "*", + "@types/topojson-specification": "*" + } + }, + "@types/topojson-simplify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/topojson-simplify/-/topojson-simplify-3.0.1.tgz", + "integrity": "sha512-H7SS2X11Lo3iRT3e7R6jPTAazOoSLD0LKIGq1b+4m/76Md46JfeU3zVIhxfIX9FY7oiyEbXwGumjK1GUXwIIMA==", + "dev": true, + "requires": { + "@types/geojson": "*", + "@types/topojson-specification": "*" + } + }, + "@types/topojson-specification": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/topojson-specification/-/topojson-specification-1.0.2.tgz", + "integrity": "sha512-SGc1NdX9g3UGDp6S+p+uyG+Z8CehS51sUJ9bejA25Xgn2kkAguILk6J9nxXK+0M/mbTBN7ypMA7+4HVLNMJ8ag==", + "dev": true, + "requires": { + "@types/geojson": "*" + } + }, + "@types/webextension-polyfill": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@types/webextension-polyfill/-/webextension-polyfill-0.8.3.tgz", + "integrity": "sha512-GN+Hjzy9mXjWoXKmaicTegv3FJ0WFZ3aYz77Wk8TMp1IY3vEzvzj1vnsa0ggV7vMI1i+PUxe4qqnIJKCzf9aTg==", + "dev": true + }, + "@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "dev": true + }, + "@types/webpack": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.0.tgz", + "integrity": "sha512-8cP0CzcxUiFuA9xGJkfeVpqmWTk9nx6CWwamRGCj95ph1SmlRRk9KlCZ6avhCbZd4L68LvYT6l1kpdEnQXrF8w==", + "dev": true, + "requires": { + "@types/node": "*", + "tapable": "^2.2.0", + "webpack": "^5" + } + }, + "@types/webpack-sources": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", + "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + } + }, + "@types/whatwg-encoding": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/whatwg-encoding/-/whatwg-encoding-2.0.3.tgz", + "integrity": "sha512-7TJfeaSFIWAKQ4ZynOb5zV3xzJQEEmL0U0j+uH7tnqfL97apXDTwMo0dB2uAWXAbr2dRRi5/eO9jV9dK/1GkiA==", + "dev": true + }, + "@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true + }, + "@types/whatwg-url": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.4.tgz", + "integrity": "sha512-lXCmTWSHJvf0TRSO58nm978b8HJ/EdsSsEKLd3ODHFjo+3VGAyyTp4v50nWvwtzBxSMQrVOK7tcuN0zGPLICMw==", + "dev": true, + "requires": { + "@types/webidl-conversions": "*" + } + }, + "@types/ws": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", + "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.59.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.6.tgz", + "integrity": "sha512-sXtOgJNEuRU5RLwPUb1jxtToZbgvq3M6FPpY4QENxoOggK+UpTxUBpj6tD8+Qh2g46Pi9We87E+eHnUw8YcGsw==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.59.6", + "@typescript-eslint/type-utils": "5.59.6", + "@typescript-eslint/utils": "5.59.6", + "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "@typescript-eslint/parser": { + "version": "5.59.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.6.tgz", + "integrity": "sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.59.6", + "@typescript-eslint/types": "5.59.6", + "@typescript-eslint/typescript-estree": "5.59.6", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.59.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.6.tgz", + "integrity": "sha512-gLbY3Le9Dxcb8KdpF0+SJr6EQ+hFGYFl6tVY8VxLPFDfUZC7BHFw+Vq7bM5lE9DwWPfx4vMWWTLGXgpc0mAYyQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.6", + "@typescript-eslint/visitor-keys": "5.59.6" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.59.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.6.tgz", + "integrity": "sha512-A4tms2Mp5yNvLDlySF+kAThV9VTBPCvGf0Rp8nl/eoDX9Okun8byTKoj3fJ52IJitjWOk0fKPNQhXEB++eNozQ==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "5.59.6", + "@typescript-eslint/utils": "5.59.6", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "@typescript-eslint/types": { + "version": "5.59.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.6.tgz", + "integrity": "sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.59.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.6.tgz", + "integrity": "sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.6", + "@typescript-eslint/visitor-keys": "5.59.6", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "dependencies": { + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "@typescript-eslint/utils": { + "version": "5.59.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.6.tgz", + "integrity": "sha512-vzaaD6EXbTS29cVH0JjXBdzMt6VBlv+hE31XktDRMX1j3462wZCJa7VzO2AxXEXcIl8GQqZPcOPuW/Z1tZVogg==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.59.6", + "@typescript-eslint/types": "5.59.6", + "@typescript-eslint/typescript-estree": "5.59.6", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.59.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.6.tgz", + "integrity": "sha512-zEfbFLzB9ETcEJ4HZEEsCR9HHeNku5/Qw1jSS5McYJv5BR+ftYXwFFAH5Al+xkGaZEqowMwl7uoJjQb1YSPF8Q==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.6", + "eslint-visitor-keys": "^3.3.0" + } + }, + "@vitejs/plugin-basic-ssl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.1.tgz", + "integrity": "sha512-pcub+YbFtFhaGRTo1832FQHQSHvMrlb43974e2eS8EKleR3p1cDdkJFPci1UhwkEf1J9Bz+wKBSzqpKp7nNj2A==", + "dev": true, + "requires": {} + }, + "@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "@yarnpkg/parsers": { + "version": "3.0.0-rc.43", + "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.43.tgz", + "integrity": "sha512-AhFF3mIDfA+jEwQv2WMHmiYhOvmdbh2qhUkDVQfiqzQtUwS4BgoWwom5NpSPg4Ix5vOul+w1690Bt21CkVLpgg==", + "dev": true, + "requires": { + "js-yaml": "^3.10.0", + "tslib": "^2.4.0" + } + }, + "@zkochan/js-yaml": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz", + "integrity": "sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + } + } + }, + "abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "peer": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "dependencies": { + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + } + } + }, + "adm-zip": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", + "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==", + "dev": true + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "agentkeepalive": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz", + "integrity": "sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "depd": "^2.0.0", + "humanize-ms": "^1.2.1" + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true + }, + "archiver": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz", + "integrity": "sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w==", + "dev": true, + "requires": { + "archiver-utils": "^2.1.0", + "async": "^3.2.3", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.0.0", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + } + }, + "archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "requires": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + } + } + }, + "array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + } + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + } + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "autoprefixer": { + "version": "10.4.14", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", + "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", + "requires": { + "browserslist": "^4.21.5", + "caniuse-lite": "^1.0.30001464", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + } + }, + "available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "requires": { + "possible-typed-array-names": "^1.0.0" + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, + "axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dev": true, + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, + "babel-loader": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz", + "integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==", + "dev": true, + "requires": { + "find-cache-dir": "^3.3.2", + "schema-utils": "^4.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "optional": true, + "peer": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "blocking-proxy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz", + "integrity": "sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + } + } + }, + "bonjour-service": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", + "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "dev": true, + "requires": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "requires": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + } + }, + "browserstack": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.6.1.tgz", + "integrity": "sha512-GxtFjpIaKdbAyzHfFDKixKO8IBT7wR3NjbzrGc78nNs/Ciys9wU3/nBtsqsWv5nDSrdI5tz0peKuzCPuNXNUiw==", + "dev": true, + "requires": { + "https-proxy-agent": "^2.2.1" + }, + "dependencies": { + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "dev": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + } + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", + "dev": true, + "requires": { + "semver": "^7.0.0" + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true + }, + "cacache": { + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.0.6.tgz", + "integrity": "sha512-ixcYmEBExFa/+ajIPjcwypxL97CjJyOsH9A/W+4qgEPIpJvKlC+HmVY8nkIck6n3PwUTdgq9c489niJGwl+5Cw==", + "dev": true, + "requires": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.4.tgz", + "integrity": "sha512-fDboBse/sl1oXSLhIp0FcCJgzW9KmhC/q8ULTKC82zc+DL3TL7FNb8qlt5qqXN53MsKEUSIcb+7DLmEygOE5Yw==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.0", + "minipass": "^5.0.0 || ^6.0.0", + "path-scurry": "^1.7.0" + } + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + }, + "minimatch": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.0.tgz", + "integrity": "sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001487", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001487.tgz", + "integrity": "sha512-83564Z3yWGqXsh2vaH/mhXfEM0wX+NlBCm1jYHOb97TrTWJEmPTccZgeLTPBUUb0PNVo+oomb7wkimZBIERClA==" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "clean-webpack-plugin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz", + "integrity": "sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w==", + "dev": true, + "requires": { + "del": "^4.1.1" + }, + "dependencies": { + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + } + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "requires": { + "is-path-inside": "^2.1.0" + } + }, + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "requires": { + "path-is-inside": "^1.0.2" + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "dev": true + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "optional": true, + "requires": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true + }, + "colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "compress-commons": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", + "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==", + "dev": true, + "requires": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + } + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true, + "optional": true, + "peer": true + } + } + }, + "connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "requires": { + "safe-buffer": "5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true, + "optional": true, + "peer": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "requires": { + "is-what": "^3.14.1" + } + }, + "copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "requires": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "dependencies": { + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + } + } + }, + "core-js-compat": { + "version": "3.30.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.30.2.tgz", + "integrity": "sha512-nriW1nuJjUgvkEjIot1Spwakz52V9YkYHZAQG6A1eCgC8AA1p0zngrQEP9R0+V6hji5XilWKG1Bd0YRppmGimA==", + "dev": true, + "requires": { + "browserslist": "^4.21.5" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "optional": true, + "requires": { + "layout-base": "^1.0.0" + } + }, + "cosmiconfig": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", + "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", + "dev": true, + "requires": { + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + } + } + }, + "cosmiconfig-typescript-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.3.0.tgz", + "integrity": "sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==", + "dev": true, + "requires": {} + }, + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true + }, + "crc32-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz", + "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", + "dev": true, + "requires": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + } + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "critters": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.16.tgz", + "integrity": "sha512-JwjgmO6i3y6RWtLYmXwO5jMd+maZt8Tnfu7VVISmEWyQqfLpB8soBswf8/2bu6SBXxtKA68Al3c+qIG1ApT68A==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "css-select": "^4.2.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "postcss": "^8.3.7", + "pretty-bytes": "^5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "css-loader": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz", + "integrity": "sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==", + "dev": true, + "requires": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.19", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "dependencies": { + "semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "cuint": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", + "integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==", + "dev": true + }, + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "dev": true, + "optional": true, + "peer": true + }, + "cytoscape": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.25.0.tgz", + "integrity": "sha512-7MW3Iz57mCUo6JQCho6CmPBCbTlJr7LzyEtIkutG255HLVd4XuBg2I9BkTZLI/e4HoaOB/BiAzXuQybQ95+r9Q==", + "optional": true, + "requires": { + "heap": "^0.2.6", + "lodash": "^4.17.21" + } + }, + "cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "optional": true, + "requires": { + "cose-base": "^1.0.0" + } + }, + "cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "optional": true, + "requires": { + "cose-base": "^2.2.0" + }, + "dependencies": { + "cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "optional": true, + "requires": { + "layout-base": "^2.0.0" + } + }, + "layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "optional": true + } + } + }, + "d3": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.4.tgz", + "integrity": "sha512-q2WHStdhiBtD8DMmhDPyJmXUxr6VWRngKyiJ5EfXMxPw+tqT6BhNjhJZ4w3BHsNm3QoVfZLY8Orq/qPFczwKRA==", + "requires": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + } + }, + "d3-array": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz", + "integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==", + "requires": { + "internmap": "1 - 2" + } + }, + "d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==" + }, + "d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + } + }, + "d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "requires": { + "d3-path": "1 - 3" + } + }, + "d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" + }, + "d3-contour": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.0.tgz", + "integrity": "sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw==", + "requires": { + "d3-array": "^3.2.0" + } + }, + "d3-delaunay": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "requires": { + "delaunator": "5" + } + }, + "d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" + }, + "d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + } + }, + "d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "requires": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + } + } + }, + "d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" + }, + "d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "requires": { + "d3-dsv": "1 - 3" + } + }, + "d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" + }, + "d3-geo": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.0.1.tgz", + "integrity": "sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==", + "requires": { + "d3-array": "2.5.0 - 3" + } + }, + "d3-hierarchy": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.1.tgz", + "integrity": "sha512-LtAIu54UctRmhGKllleflmHalttH3zkfSi4NlKrTAoFKjC+AFBJohsCAdgCBYQwH0F8hIOGY89X1pPqAchlMkA==" + }, + "d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "requires": { + "d3-color": "1 - 3" + } + }, + "d3-path": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz", + "integrity": "sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==" + }, + "d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==" + }, + "d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==" + }, + "d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==" + }, + "d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "requires": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + } + }, + "d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "requires": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + } + }, + "d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" + }, + "d3-shape": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz", + "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==", + "requires": { + "d3-path": "1 - 3" + } + }, + "d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==", + "requires": { + "d3-array": "2 - 3" + } + }, + "d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "requires": { + "d3-time": "1 - 3" + } + }, + "d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" + }, + "d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "requires": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + } + }, + "dagre-d3-es": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.9.tgz", + "integrity": "sha512-rYR4QfVmy+sR44IBDvVtcAmOReGBvRCWDpO2QjYwqgh9yijw6eSHBqaPG/LIOEy7aBsniLvtMW6pg19qJhq60w==", + "optional": true, + "requires": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "requires": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + } + }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } + }, + "date-format": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.11.tgz", + "integrity": "sha512-VS20KRyorrbMCQmpdl2hg5KaOUsda1RbnsJg461FfrcyCUg+pkd0b40BSW4niQyTheww4DBXQnS7HwSrKkipLw==", + "dev": true, + "optional": true, + "peer": true + }, + "dayjs": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==", + "optional": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-equal": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.1.tgz", + "integrity": "sha512-lKdkdV6EOGoVn65XaOsPdH4rMxTZOnmFyuIkMjM1i5HHCbfjC97dawgTAy0deYNfuqUqW+Q5VrVaQYtUpSd6yQ==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.0", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.0", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, + "default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "requires": { + "execa": "^5.0.0" + } + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true + }, + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "^5.0.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "rimraf": "^2.2.8" + }, + "dependencies": { + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "requires": { + "robust-predicates": "^3.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "optional": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, + "dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true + }, + "detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "dev": true, + "optional": true, + "peer": true + }, + "didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", + "dev": true + }, + "dns-packet": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.0.tgz", + "integrity": "sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ==", + "dev": true, + "requires": { + "@leichtgewicht/ip-codec": "^2.0.1" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "requires": { + "domelementtype": "^2.2.0" + } + }, + "dompurify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz", + "integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==", + "optional": true + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true + }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dev": true, + "requires": { + "jake": "^10.8.5" + } + }, + "electron-to-chromium": { + "version": "1.4.396", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.396.tgz", + "integrity": "sha512-pqKTdqp/c5vsrc0xUPYXTDBo9ixZuGY8es4ZOjjd6HD6bFYbu5QA09VoW3fkY4LF1T0zYk86lN6bZnNlBuOpdQ==" + }, + "elkjs": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", + "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==", + "optional": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "emoji-toolkit": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-7.0.1.tgz", + "integrity": "sha512-l5aJyAhpC5s4mDuoVuqt4SzVjwIsIvakPh4ZGJJE4KWuWFCEHaXacQFkStVdD9zbRR+/BbRXob7u99o0lQFr8A==" + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "engine.io": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz", + "integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.11.0" + } + }, + "engine.io-parser": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", + "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==", + "dev": true, + "optional": true, + "peer": true + }, + "enhanced-resolve": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.0.tgz", + "integrity": "sha512-+DCows0XNwLDcUhbFJPdlQEVnT2zXlCv7hPxemTz86/O+B/hCQ+mb7ydkPKiflpVraqLPCAfu7lDy+hBXueojw==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true, + "optional": true, + "peer": true + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true + }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true + }, + "err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.22.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.5.tgz", + "integrity": "sha512-oW69R+4q2wG+Hc3KZePPZxOiisRIqfKBVo/HLx94QcJeWGU/8sZhCvc829rd1kS366vlJbzBfXf9yWwf0+Ko7w==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.1", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.0", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.5", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.14" + } + }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true + }, + "es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, + "es-module-lexer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", + "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", + "dev": true + }, + "es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "^4.0.3" + } + }, + "esbuild": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.18.tgz", + "integrity": "sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.17.18", + "@esbuild/android-arm64": "0.17.18", + "@esbuild/android-x64": "0.17.18", + "@esbuild/darwin-arm64": "0.17.18", + "@esbuild/darwin-x64": "0.17.18", + "@esbuild/freebsd-arm64": "0.17.18", + "@esbuild/freebsd-x64": "0.17.18", + "@esbuild/linux-arm": "0.17.18", + "@esbuild/linux-arm64": "0.17.18", + "@esbuild/linux-ia32": "0.17.18", + "@esbuild/linux-loong64": "0.17.18", + "@esbuild/linux-mips64el": "0.17.18", + "@esbuild/linux-ppc64": "0.17.18", + "@esbuild/linux-riscv64": "0.17.18", + "@esbuild/linux-s390x": "0.17.18", + "@esbuild/linux-x64": "0.17.18", + "@esbuild/netbsd-x64": "0.17.18", + "@esbuild/openbsd-x64": "0.17.18", + "@esbuild/sunos-x64": "0.17.18", + "@esbuild/win32-arm64": "0.17.18", + "@esbuild/win32-ia32": "0.17.18", + "@esbuild/win32-x64": "0.17.18" + } + }, + "esbuild-wasm": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.17.18.tgz", + "integrity": "sha512-h4m5zVa+KaDuRFIbH9dokMwovvkIjTQJS7/Ry+0Z1paVuS9aIkso2vdA2GmwH9GSvGX6w71WveJ3PfkoLuWaRw==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "eslint": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz", + "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.3", + "@eslint/js": "8.40.0", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.5.2", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "dev": true + }, + "espree": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "dev": true, + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "dependencies": { + "acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true + }, + "eventemitter-asyncresource": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", + "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==", + "dev": true + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dev": true, + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true, + "optional": true, + "peer": true + } + } + }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz", + "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==", + "dev": true + }, + "follow-redirects": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "dev": true + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, + "foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", + "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", + "dev": true + } + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true + }, + "fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs-minipass": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.2.tgz", + "integrity": "sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g==", + "dev": true, + "requires": { + "minipass": "^5.0.0" + } + }, + "fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "fuse.js": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz", + "integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==" + }, + "gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "dev": true, + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + } + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "globby": { + "version": "13.1.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz", + "integrity": "sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==", + "dev": true, + "requires": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + } + }, + "good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "optional": true, + "requires": { + "delegate": "^3.1.2" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graceful-fs": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", + "dev": true + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "requires": { + "duplexer": "^0.1.2" + } + }, + "handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "dev": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + } + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.3" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true + }, + "hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "hdr-histogram-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", + "integrity": "sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==", + "dev": true, + "requires": { + "@assemblyscript/loader": "^0.10.1", + "base64-js": "^1.2.0", + "pako": "^1.0.3" + } + }, + "hdr-histogram-percentiles-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", + "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", + "dev": true + }, + "heap": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", + "optional": true + }, + "hosted-git-info": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", + "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", + "dev": true, + "requires": { + "lru-cache": "^7.5.1" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + } + } + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "dev": true + }, + "http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "dependencies": { + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + } + } + }, + "http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "requires": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "requires": { + "ms": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "requires": {} + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "ignore-walk": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.3.tgz", + "integrity": "sha512-C7FfFoTA+bI10qfeydT8aZbvr91vAEU+2W5BZUlzPec47oNb07SsOfwYrtxuvOYdUApPP/Qlh4DtAO51Ekk2QA==", + "dev": true, + "requires": { + "minimatch": "^9.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.0.tgz", + "integrity": "sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", + "dev": true + }, + "immutable": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", + "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.0.0.tgz", + "integrity": "sha512-t0ikzf5qkSFqRl1e6ejKBe+Tk2bsQd8ivEkcisyGXsku2t8NvXZ1Y3RRz5vxrDgOrTBOi13CvGsVoI5wVpd7xg==", + "dev": true + }, + "injection-js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/injection-js/-/injection-js-2.4.0.tgz", + "integrity": "sha512-6jiJt0tCAo9zjHbcwLiPL+IuNe9SQ6a9g0PEzafThW3fOQi0mrmiJGBJvDD6tmhPh8cQHIQtCOrJuBfQME4kPA==", + "dev": true, + "requires": { + "tslib": "^2.0.0" + } + }, + "inquirer": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", + "integrity": "sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + } + }, + "internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" + }, + "ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "dev": true + }, + "ipaddr.js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", + "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", + "dev": true + }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "requires": { + "builtin-modules": "^3.3.0" + }, + "dependencies": { + "builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true + } + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true + }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true + }, + "is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "dev": true, + "requires": { + "is-path-inside": "^1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.14" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isbinaryfile": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.8.tgz", + "integrity": "sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w==", + "dev": true, + "optional": true, + "peer": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "jackspeak": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.2.0.tgz", + "integrity": "sha512-r5XBrqIJfwRIjRt/Xr5fv9Wh09qyhHfKnYddDlpM+ibRR20qrYActpCAgU6U+d53EOEjzkvxPMVHSlgR7leXrQ==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "jake": { + "version": "10.8.6", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.6.tgz", + "integrity": "sha512-G43Ub9IYEFfu72sua6rzooi8V8Gz2lkfk48rW20vEWCGizeaEPlKB1Kh8JIA84yQbiAEfqlPmSpGgCKKxH3rDA==", + "dev": true, + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jasmine": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.8.0.tgz", + "integrity": "sha1-awicChFXax8W3xG4AUbZHU6Lij4=", + "dev": true, + "requires": { + "exit": "^0.1.2", + "glob": "^7.0.6", + "jasmine-core": "~2.8.0" + }, + "dependencies": { + "jasmine-core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz", + "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=", + "dev": true + } + } + }, + "jasmine-core": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.0.0.tgz", + "integrity": "sha512-BJLxZlSVyWPN/oyaS1IIvIjChghI9/xWsLAIJqL9J5Fz47CN3JNr8Lmik3S2S7QS2RxclYjvSVSXP7IR35PAmg==", + "dev": true + }, + "jasmine-spec-reporter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-7.0.0.tgz", + "integrity": "sha512-OtC7JRasiTcjsaCBPtMO0Tl8glCejM4J4/dNuOJdA8lBjz4PmWjYQ6pzb0uzpBNAWJMDudYuj9OdXJWqM2QTJg==", + "dev": true, + "requires": { + "colors": "1.4.0" + } + }, + "jasminewd2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jasminewd2/-/jasminewd2-2.2.0.tgz", + "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=", + "dev": true + }, + "jest-worker": { + "version": "27.4.6", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.6.tgz", + "integrity": "sha512-gHWJF/6Xi5CTG5QCvROr6GcmpIqNYpDJyc8A1h/DyXqH1tD6SnRCM0d3U5msV31D2LB/U+E0M+W4oyvKV44oNw==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jiti": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", + "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==", + "dev": true + }, + "js-sdsl": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", + "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "js-yaml-loader": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/js-yaml-loader/-/js-yaml-loader-1.2.2.tgz", + "integrity": "sha512-H+NeuNrG6uOs/WMjna2SjkaCw13rMWiT/D7l9+9x5n8aq88BDsh2sRmdfxckWPIHtViYHWRG6XiCKYvS1dfyLg==", + "dev": true, + "requires": { + "js-yaml": "^3.13.1", + "loader-utils": "^1.2.3", + "un-eval": "^1.2.0" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + } + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + }, + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true + }, + "jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "karma": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.2.tgz", + "integrity": "sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.4.1", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "peer": true + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "rimraf": "^3.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "optional": true, + "peer": true + } + } + }, + "karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "requires": { + "source-map-support": "^0.5.5" + } + }, + "katex": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.0.tgz", + "integrity": "sha512-wPRB4iUPysfH97wTgG5/tRLYxmKVq6Q4jRAWRVOUxXB1dsiv4cvcNjqabHkrOvJHM1Bpk3WrgmllSO1vIvP24w==", + "optional": true, + "requires": { + "commander": "^8.0.0" + }, + "dependencies": { + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "optional": true + } + } + }, + "khroma": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", + "integrity": "sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==", + "optional": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true + }, + "launch-editor": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", + "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "dev": true, + "requires": { + "picocolors": "^1.0.0", + "shell-quote": "^1.7.3" + } + }, + "layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "optional": true + }, + "lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "requires": { + "readable-stream": "^2.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "less": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", + "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==", + "dev": true, + "requires": { + "copy-anything": "^2.0.1", + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "parse-node-version": "^1.0.1", + "source-map": "~0.6.0", + "tslib": "^2.3.0" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "optional": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + } + } + }, + "less-loader": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz", + "integrity": "sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==", + "dev": true, + "requires": { + "klona": "^2.0.4" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "requires": { + "webpack-sources": "^3.0.0" + } + }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "requires": { + "immediate": "~3.0.5" + } + }, + "lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true + } + } + }, + "loader-runner": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", + "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "dev": true + }, + "loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true + }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "optional": true + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true + }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "log4js": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.6.0.tgz", + "integrity": "sha512-3v8R7fd45UB6THucSht6wN2/7AZEruQbXdjygPZcxt5TA/msO6si9CN5MefUuKXbYnJHTBnYcx4famwcyQd+sA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "date-format": "^4.0.11", + "debug": "^4.3.4", + "flatted": "^3.2.5", + "rfdc": "^1.3.0", + "streamroller": "^3.1.1" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "magic-string": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.13" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "requires": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "dependencies": { + "@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "requires": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "requires": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + } + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "requires": { + "minipass": "^3.1.1" + } + }, + "unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "requires": { + "unique-slug": "^3.0.0" + } + }, + "unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + } + } + }, + "marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "peer": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "memfs": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.1.tgz", + "integrity": "sha512-UWbFJKvj5k+nETdteFndTpYxdeTMox/ULeqX5k/dpaQJCCFmj5EeKv3dBcyO2xmkRAx2vppRu5dVG7SOtsGOzA==", + "dev": true, + "requires": { + "fs-monkey": "^1.0.3" + } + }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "mermaid": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-9.4.3.tgz", + "integrity": "sha512-TLkQEtqhRSuEHSE34lh5bCa94KATCyluAXmFnNI2PRZwOpXFeqiJWwZl+d2CcemE1RS6QbbueSSq9QIg8Uxcyw==", + "optional": true, + "requires": { + "@braintree/sanitize-url": "^6.0.0", + "cytoscape": "^3.23.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.1.0", + "d3": "^7.4.0", + "dagre-d3-es": "7.0.9", + "dayjs": "^1.11.7", + "dompurify": "2.4.3", + "elkjs": "^0.8.2", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.2", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + }, + "dependencies": { + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "optional": true + } + } + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "optional": true, + "peer": true + }, + "mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "dev": true + }, + "mime-types": { + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "dev": true, + "requires": { + "mime-db": "1.51.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mini-css-extract-plugin": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.5.tgz", + "integrity": "sha512-9HaR++0mlgom81s95vvNjxkg52n2b5s//3ZTI1EtzFb98awsLSivs2LMsVqnQ3ay0PVhqWcGNyDaTE961FOcjQ==", + "dev": true, + "requires": { + "schema-utils": "^4.0.0" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-json-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "dev": true, + "requires": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "requires": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + } + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "requires": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "needle": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz", + "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==", + "dev": true, + "optional": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "ng-packagr": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-16.0.1.tgz", + "integrity": "sha512-MiJvSR+8olzCViwkQ6ihHLFWVNLdsfUNPCxrZqR7u1nOC/dXlWPf//l2IG0KLdVhHNCiM64mNdwaTpgDEBMD3w==", + "dev": true, + "requires": { + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.0.0", + "ajv": "^8.11.0", + "ansi-colors": "^4.1.3", + "autoprefixer": "^10.4.12", + "browserslist": "^4.21.4", + "cacache": "^17.0.0", + "chokidar": "^3.5.3", + "commander": "^10.0.0", + "convert-source-map": "^2.0.0", + "dependency-graph": "^0.11.0", + "esbuild": "^0.17.0", + "esbuild-wasm": "^0.17.0", + "fast-glob": "^3.2.12", + "find-cache-dir": "^3.3.2", + "injection-js": "^2.4.0", + "jsonc-parser": "^3.2.0", + "less": "^4.1.3", + "ora": "^5.1.0", + "piscina": "^3.2.0", + "postcss": "^8.4.16", + "postcss-url": "^10.1.3", + "rollup": "^3.0.0", + "rxjs": "^7.5.6", + "sass": "^1.55.0" + }, + "dependencies": { + "commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + } + } + }, + "ng-zorro-antd": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/ng-zorro-antd/-/ng-zorro-antd-16.1.0.tgz", + "integrity": "sha512-+KjXoA0+v/liTtVIHswmOAzB9UaGADrO1tL9AOZsTLq5sZM8+DmhtixGRoSMD8HkkhpMFhsgEIxoHlkxtn1SXg==", + "requires": { + "@angular/cdk": "^16.0.0", + "@ant-design/icons-angular": "^16.0.0", + "date-fns": "^2.16.1", + "tslib": "^2.3.0" + } + }, + "ngx-markdown": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/ngx-markdown/-/ngx-markdown-16.0.0.tgz", + "integrity": "sha512-/rlbXi+HBscJCDdwaTWIUrRkvwJicPnuAgeugOCZa0UbZ4VCWV3U0+uB1Zv6krRDF6FXJNXNLTUrMZV7yH8I6A==", + "requires": { + "clipboard": "^2.0.11", + "emoji-toolkit": "^7.0.0", + "katex": "^0.16.0", + "mermaid": "^9.1.2", + "prismjs": "^1.28.0", + "tslib": "^2.3.0" + } + }, + "nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "optional": true, + "requires": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true + }, + "node-gyp": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.3.1.tgz", + "integrity": "sha512-4Q16ZCqq3g8awk6UplT7AuxQ35XN4R/yf/+wSAwcBUAjg7l58RTactWaP8fIDTi0FzI7YcVLujwExakZlfWkXg==", + "dev": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "node-gyp-build": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz", + "integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==", + "dev": true + }, + "node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==" + }, + "non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", + "optional": true + }, + "nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "requires": { + "abbrev": "^1.0.0" + } + }, + "normalize-package-data": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", + "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", + "dev": true, + "requires": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" + }, + "npm-bundled": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.0.tgz", + "integrity": "sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ==", + "dev": true, + "requires": { + "npm-normalize-package-bin": "^3.0.0" + } + }, + "npm-install-checks": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.1.1.tgz", + "integrity": "sha512-dH3GmQL4vsPtld59cOn8uY0iOqRmqKvV+DLGwNXV/Q7MDgD2QfOADWd/mFXcIE5LVhYYGjA3baz6W9JneqnuCw==", + "dev": true, + "requires": { + "semver": "^7.1.1" + } + }, + "npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true + }, + "npm-package-arg": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz", + "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==", + "dev": true, + "requires": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + } + }, + "npm-packlist": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-7.0.4.tgz", + "integrity": "sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==", + "dev": true, + "requires": { + "ignore-walk": "^6.0.0" + } + }, + "npm-pick-manifest": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.1.tgz", + "integrity": "sha512-mRtvlBjTsJvfCCdmPtiu2bdlx8d/KXtF7yNXNWe7G0Z36qWA9Ny5zXsI2PfBZEv7SXgoxTmNaTzGSbbzDZChoA==", + "dev": true, + "requires": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + } + }, + "npm-registry-fetch": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz", + "integrity": "sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA==", + "dev": true, + "requires": { + "make-fetch-happen": "^11.0.0", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^10.0.0", + "proc-log": "^3.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + }, + "make-fetch-happen": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", + "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", + "dev": true, + "requires": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + } + }, + "minipass-fetch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.3.tgz", + "integrity": "sha512-n5ITsTkDqYkYJZjcRWzZt9qnZKCT7nKCosJhHoj7S7zD+BP4jVbWs+odsniw5TA3E0sLomhTKOKjF86wf11PuQ==", + "dev": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^5.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + } + } + } + }, + "npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + } + } + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "dev": true, + "requires": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + } + }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, + "nx": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/nx/-/nx-16.0.2.tgz", + "integrity": "sha512-8Z9Bo1D2VbYjyC/F2ONensKjm10snz1UfkzURZiFA+oXikBPldiH1u67TOTpoCYZfyYQg4l6h6EpOaAvHF6Abg==", + "dev": true, + "requires": { + "@nrwl/tao": "16.0.2", + "@nx/nx-darwin-arm64": "16.0.2", + "@nx/nx-darwin-x64": "16.0.2", + "@nx/nx-linux-arm-gnueabihf": "16.0.2", + "@nx/nx-linux-arm64-gnu": "16.0.2", + "@nx/nx-linux-arm64-musl": "16.0.2", + "@nx/nx-linux-x64-gnu": "16.0.2", + "@nx/nx-linux-x64-musl": "16.0.2", + "@nx/nx-win32-arm64-msvc": "16.0.2", + "@nx/nx-win32-x64-msvc": "16.0.2", + "@parcel/watcher": "2.0.4", + "@yarnpkg/lockfile": "^1.1.0", + "@yarnpkg/parsers": "^3.0.0-rc.18", + "@zkochan/js-yaml": "0.0.6", + "axios": "^1.0.0", + "chalk": "^4.1.0", + "cli-cursor": "3.1.0", + "cli-spinners": "2.6.1", + "cliui": "^7.0.2", + "dotenv": "~10.0.0", + "enquirer": "~2.3.6", + "fast-glob": "3.2.7", + "figures": "3.2.0", + "flat": "^5.0.2", + "fs-extra": "^11.1.0", + "glob": "7.1.4", + "ignore": "^5.0.4", + "js-yaml": "4.1.0", + "jsonc-parser": "3.2.0", + "lines-and-columns": "~2.0.3", + "minimatch": "3.0.5", + "npm-run-path": "^4.0.1", + "open": "^8.4.0", + "semver": "7.3.4", + "string-width": "^4.2.3", + "strong-log-transformer": "^2.1.0", + "tar-stream": "~2.2.0", + "tmp": "~0.2.1", + "tsconfig-paths": "^4.1.2", + "tslib": "^2.3.0", + "v8-compile-cache": "2.3.0", + "yargs": "^17.6.2", + "yargs-parser": "21.1.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "lines-and-columns": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", + "integrity": "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==", + "dev": true + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + } + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true + }, + "object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true + }, + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "requires": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } + } + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "requires": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "dependencies": { + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true + } + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pacote": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.1.3.tgz", + "integrity": "sha512-aRts8cZqxiJVDitmAh+3z+FxuO3tLNWEmwDRPEpDDiZJaRz06clP4XX112ynMT5uF0QNoMPajBBHnaStUEPJXA==", + "dev": true, + "requires": { + "@npmcli/git": "^4.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^6.0.1", + "@npmcli/run-script": "^6.0.0", + "cacache": "^17.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^5.0.0", + "npm-package-arg": "^10.0.0", + "npm-packlist": "^7.0.0", + "npm-pick-manifest": "^8.0.0", + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^6.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^1.3.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + } + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true + }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "devOptional": true, + "requires": { + "entities": "^4.4.0" + }, + "dependencies": { + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "devOptional": true + } + } + }, + "parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "requires": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "dependencies": { + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + } + } + }, + "parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "requires": { + "parse5": "^6.0.1" + }, + "dependencies": { + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + } + } + }, + "parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "requires": { + "parse5": "^7.0.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-scurry": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.9.1.tgz", + "integrity": "sha512-UgmoiySyjFxP6tscZDgWGEAgsW5ok8W3F5CJDnnH2pozwSTGE6eH7vwTotMwATWA2r5xqdkKdxYPkwlJjAI/3g==", + "dev": true, + "requires": { + "lru-cache": "^9.1.1", + "minipass": "^5.0.0 || ^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.1.tgz", + "integrity": "sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A==", + "dev": true + } + } + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "dev": true + }, + "piscina": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-3.2.0.tgz", + "integrity": "sha512-yn/jMdHRw+q2ZJhFhyqsmANcbF6V2QwmD84c6xRau+QpQOmtrBCoRGdvTfeuFDYXB5W2m6MfLkjkvQa9lUSmIA==", + "dev": true, + "requires": { + "eventemitter-asyncresource": "^1.0.0", + "hdr-histogram-js": "^2.0.1", + "hdr-histogram-percentiles-obj": "^3.0.0", + "nice-napi": "^1.0.2" + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true + }, + "postcss": { + "version": "8.4.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", + "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + } + }, + "postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "requires": { + "camelcase-css": "^2.0.1" + } + }, + "postcss-load-config": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "dev": true, + "requires": { + "lilconfig": "^2.0.5", + "yaml": "^2.1.1" + } + }, + "postcss-loader": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.0.tgz", + "integrity": "sha512-qLAFjvR2BFNz1H930P7mj1iuWJFjGey/nVhimfOAAQ1ZyPpcClAxP8+A55Sl8mBvM+K2a9Pjgdj10KpANWrNfw==", + "dev": true, + "requires": { + "cosmiconfig": "^8.1.3", + "jiti": "^1.18.2", + "klona": "^2.0.6", + "semver": "^7.3.8" + }, + "dependencies": { + "semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "requires": {} + }, + "postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0" + } + }, + "postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.11" + } + }, + "postcss-scss": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz", + "integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==", + "dev": true, + "requires": {} + }, + "postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-url": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-10.1.3.tgz", + "integrity": "sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw==", + "dev": true, + "requires": { + "make-dir": "~3.1.0", + "mime": "~2.5.2", + "minimatch": "~3.0.4", + "xxhashjs": "~0.2.2" + }, + "dependencies": { + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "dev": true + } + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true + }, + "prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==" + }, + "proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, + "promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } + }, + "protractor": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/protractor/-/protractor-7.0.0.tgz", + "integrity": "sha512-UqkFjivi4GcvUQYzqGYNe0mLzfn5jiLmO8w9nMhQoJRLhy2grJonpga2IWhI6yJO30LibWXJJtA4MOIZD2GgZw==", + "dev": true, + "requires": { + "@types/q": "^0.0.32", + "@types/selenium-webdriver": "^3.0.0", + "blocking-proxy": "^1.0.0", + "browserstack": "^1.5.1", + "chalk": "^1.1.3", + "glob": "^7.0.3", + "jasmine": "2.8.0", + "jasminewd2": "^2.1.0", + "q": "1.4.1", + "saucelabs": "^1.5.0", + "selenium-webdriver": "3.6.0", + "source-map-support": "~0.4.0", + "webdriver-js-extender": "2.1.0", + "webdriver-manager": "^12.1.7", + "yargs": "^15.3.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "^0.5.6" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "dependencies": { + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + } + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + }, + "purgecss": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-5.0.0.tgz", + "integrity": "sha512-RAnuxrGuVyLLTr8uMbKaxDRGWMgK5CCYDfRyUNNcaz5P3kGgD2b7ymQGYEyo2ST7Tl/ScwFgf5l3slKMxHSbrw==", + "dev": true, + "requires": { + "commander": "^9.0.0", + "glob": "^8.0.3", + "postcss": "^8.4.4", + "postcss-selector-parser": "^6.0.7" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "q": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", + "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", + "dev": true + }, + "qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "optional": true, + "peer": true + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "dev": true, + "requires": { + "pify": "^2.3.0" + } + }, + "read-package-json": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.3.tgz", + "integrity": "sha512-4QbpReW4kxFgeBQ0vPAqh2y8sXEB3D4t3jsXbJKIhBiF80KT6XRo45reqwtftju5J6ru1ax06A2Gb/wM1qCOEQ==", + "dev": true, + "requires": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.4.tgz", + "integrity": "sha512-fDboBse/sl1oXSLhIp0FcCJgzW9KmhC/q8ULTKC82zc+DL3TL7FNb8qlt5qqXN53MsKEUSIcb+7DLmEygOE5Yw==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.0", + "minipass": "^5.0.0 || ^6.0.0", + "path-scurry": "^1.7.0" + } + }, + "json-parse-even-better-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", + "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", + "dev": true + }, + "minimatch": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.0.tgz", + "integrity": "sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "requires": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "dependencies": { + "json-parse-even-better-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", + "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", + "dev": true + } + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdir-glob": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.2.tgz", + "integrity": "sha512-6RLVvwJtVwEDfPdn6X6Ille4/lxGl0ATOY4FN/B9nxQcgOazvvI0nodiD19ScKq0PvA/29VpaOQML36o5IzZWA==", + "dev": true, + "requires": { + "minimatch": "^5.1.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dev": true, + "requires": { + "regenerate": "^1.4.2" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "dev": true + }, + "regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + } + }, + "regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "requires": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + } + }, + "regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true + } + } + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "requires": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "requires": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "dependencies": { + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true, + "optional": true, + "peer": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "robust-predicates": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.1.tgz", + "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==" + }, + "rollup": { + "version": "3.21.8", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.8.tgz", + "integrity": "sha512-SSFV2T2fWtQ/vvBip85u2Nr0GNKireabH9d7nXswBg+XSH+jbVDSYptRAEbCEsquhs503rpPA9POYAp0/Jhasw==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "requires": { + "tslib": "^2.1.0" + } + }, + "safe-array-concat": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", + "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sass": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz", + "integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, + "sass-loader": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.2.2.tgz", + "integrity": "sha512-nrIdVAAte3B9icfBiGWvmMhT/D+eCDwnk+yA7VE/76dp/WkHX+i44Q/pfo71NYbwj0Ap+PGsn0ekOuU1WFJ2AA==", + "dev": true, + "requires": { + "klona": "^2.0.6", + "neo-async": "^2.6.2" + } + }, + "saucelabs": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.5.0.tgz", + "integrity": "sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==", + "dev": true, + "requires": { + "https-proxy-agent": "^2.2.1" + }, + "dependencies": { + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "dev": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "schema-utils": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", + "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + }, + "select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==", + "optional": true + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "selenium-webdriver": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz", + "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==", + "dev": true, + "requires": { + "jszip": "^3.1.3", + "rimraf": "^2.5.4", + "tmp": "0.0.30", + "xml2js": "^0.4.17" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "tmp": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", + "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.1" + } + } + } + }, + "selfsigned": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "dev": true, + "requires": { + "node-forge": "^1" + } + }, + "semver": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", + "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-function-length": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "dev": true, + "requires": { + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + } + }, + "set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "sigstore": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-1.5.1.tgz", + "integrity": "sha512-FIPThk7S1oeFXn8O8yh7gpyiQb6lYXzMIlOBzXhId/f81VvU587xNCHc4jd2lZ9724UkKUYYTuKSYcjhDSRD/Q==", + "dev": true, + "requires": { + "@sigstore/protobuf-specs": "^0.1.0", + "make-fetch-happen": "^11.0.1", + "tuf-js": "^1.1.3" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + }, + "make-fetch-happen": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", + "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", + "dev": true, + "requires": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + } + }, + "minipass-fetch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.3.tgz", + "integrity": "sha512-n5ITsTkDqYkYJZjcRWzZt9qnZKCT7nKCosJhHoj7S7zD+BP4jVbWs+odsniw5TA3E0sLomhTKOKjF86wf11PuQ==", + "dev": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^5.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + } + } + } + }, + "sirv": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", + "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "dev": true, + "requires": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^1.0.0" + } + }, + "slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true + }, + "socket.io": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", + "integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.4.1", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.1" + } + }, + "socket.io-adapter": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "ws": "~8.11.0" + } + }, + "socket.io-parser": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.3.tgz", + "integrity": "sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + } + }, + "sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "requires": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "socks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "dev": true, + "requires": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + } + }, + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + }, + "source-map-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz", + "integrity": "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==", + "dev": true, + "requires": { + "abab": "^2.0.6", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + } + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "dev": true + }, + "spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "sshpk": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.4.tgz", + "integrity": "sha512-12+IR2CB2C28MMAw0Ncqwj5QbTcs0nGIhgJzYWzDkb21vWmfNI83KS4f3Ci6GI98WreIfG7o9UXp3C0qbpA8nQ==", + "dev": true, + "requires": { + "minipass": "^5.0.0" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "requires": { + "internal-slot": "^1.0.4" + } + }, + "streamroller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.1.tgz", + "integrity": "sha512-iPhtd9unZ6zKdWgMeYGfSBuqCngyJy1B/GPi/lTpwGpa3bajuX30GjUVd0/Tn/Xhg0mr4DOSENozz9Y06qyonQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "date-format": "^4.0.10", + "debug": "^4.3.4", + "fs-extra": "^10.1.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string.prototype.padend": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.5.tgz", + "integrity": "sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "strong-log-transformer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz", + "integrity": "sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==", + "dev": true, + "requires": { + "duplexer": "^0.1.1", + "minimist": "^1.2.0", + "through": "^2.3.4" + } + }, + "stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "optional": true + }, + "stylus": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz", + "integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@adobe/css-tools": "^4.0.1", + "debug": "^4.3.2", + "glob": "^7.1.6", + "sax": "~1.2.4", + "source-map": "^0.7.3" + } + }, + "sucrase": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz", + "integrity": "sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "dependencies": { + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true + }, + "tailwindcss": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz", + "integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==", + "dev": true, + "requires": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.18.2", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "dependencies": { + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + } + } + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + }, + "tar": { + "version": "6.1.14", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.14.tgz", + "integrity": "sha512-piERznXu0U7/pW7cdSn7hjqySIVTYT6F76icmFk7ptU7dDYlXTm5r9A6K04R2vU3olYgoKeo1Cg3eeu5nhftAw==", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + } + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "terser": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.1.tgz", + "integrity": "sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.8.tgz", + "integrity": "sha512-WiHL3ElchZMsK27P8uIUh4604IgJyAW47LVXGbEoB21DbQcZ+OuMpGjVYnEUaqcWM6dO8uS2qUbA7LSCWqvsbg==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "dependencies": { + "acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "terser": { + "version": "5.17.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.3.tgz", + "integrity": "sha512-AudpAZKmZHkG9jueayypz4duuCFJMMNGRMwaPvQKWfxKedh8Z2x3OCoDqIIi1xx5+iwx1u6Au8XQcc9Lke65Yg==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + } + } + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "requires": { + "any-promise": "^1.0.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "requires": { + "thenify": ">= 3.1.0 < 4" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "optional": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true + }, + "topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "requires": { + "commander": "2" + } + }, + "topojson-simplify": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/topojson-simplify/-/topojson-simplify-3.0.3.tgz", + "integrity": "sha512-V+pBjLVzSQ3+hSOxBiV01OVXgFiCmMO8ia3huxKEyIMTC1ApQHBcdXdOqcQ6U2JJJD31TZduwY6KyF15R8sUgg==", + "requires": { + "commander": "2", + "topojson-client": "3" + } + }, + "totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "dev": true + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "requires": { + "punycode": "^2.3.1" + } + }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, + "ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "optional": true + }, + "ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "dependencies": { + "acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "dev": true + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + } + } + }, + "tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "requires": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "tslint": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", + "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.3", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.13.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "tuf-js": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-1.1.6.tgz", + "integrity": "sha512-CXwFVIsXGbVY4vFiWF7TJKWmlKJAT8TWkH4RmiohJRcDJInix++F0dznDmoVbtJNzZ8yLprKUG4YrDIhv3nBMg==", + "dev": true, + "requires": { + "@tufjs/models": "1.0.4", + "debug": "^4.3.4", + "make-fetch-happen": "^11.1.0" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + }, + "make-fetch-happen": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", + "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", + "dev": true, + "requires": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + } + }, + "minipass-fetch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.3.tgz", + "integrity": "sha512-n5ITsTkDqYkYJZjcRWzZt9qnZKCT7nKCosJhHoj7S7zD+BP4jVbWs+odsniw5TA3E0sLomhTKOKjF86wf11PuQ==", + "dev": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^5.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + } + } + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.5.tgz", + "integrity": "sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + } + }, + "typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true + }, + "typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==" + }, + "ua-parser-js": { + "version": "0.7.35", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz", + "integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==", + "dev": true, + "optional": true, + "peer": true + }, + "un-eval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/un-eval/-/un-eval-1.2.0.tgz", + "integrity": "sha512-Wlj/pum6dQtGTPD/lclDtoVPkSfpjPfy1dwnnKw/sZP5DpBH9fLhBgQfsqNhe5/gS1D+vkZUuB771NRMUPA5CA==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true + }, + "unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "requires": { + "unique-slug": "^4.0.0" + } + }, + "unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "useragent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", + "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", + "dev": true, + "requires": { + "lru-cache": "4.1.x", + "tmp": "0.0.x" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validate-npm-package-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", + "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", + "dev": true, + "requires": { + "builtins": "^5.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vite": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.1.tgz", + "integrity": "sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==", + "dev": true, + "requires": { + "esbuild": "^0.17.5", + "fsevents": "~2.3.2", + "postcss": "^8.4.21", + "rollup": "^3.20.2" + } + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true, + "optional": true, + "peer": true + }, + "watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "web-worker": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", + "integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==", + "optional": true + }, + "webdriver-js-extender": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz", + "integrity": "sha512-lcUKrjbBfCK6MNsh7xaY2UAUmZwe+/ib03AjVOpFobX4O7+83BUveSrLfU0Qsyb1DaKJdQRbuU+kM9aZ6QUhiQ==", + "dev": true, + "requires": { + "@types/selenium-webdriver": "^3.0.0", + "selenium-webdriver": "^3.0.1" + } + }, + "webdriver-manager": { + "version": "12.1.9", + "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.9.tgz", + "integrity": "sha512-Yl113uKm8z4m/KMUVWHq1Sjtla2uxEBtx2Ue3AmIlnlPAKloDn/Lvmy6pqWCUersVISpdMeVpAaGbNnvMuT2LQ==", + "dev": true, + "requires": { + "adm-zip": "^0.5.2", + "chalk": "^1.1.1", + "del": "^2.2.0", + "glob": "^7.0.3", + "ini": "^1.3.4", + "minimist": "^1.2.0", + "q": "^1.4.1", + "request": "^2.87.0", + "rimraf": "^2.5.2", + "semver": "^5.3.0", + "xml2js": "^0.4.17" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "webextension-polyfill": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.8.0.tgz", + "integrity": "sha512-a19+DzlT6Kp9/UI+mF9XQopeZ+n2ussjhxHJ4/pmIGge9ijCDz7Gn93mNnjpZAk95T4Tae8iHZ6sSf869txqiQ==", + "dev": true + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" + }, + "webpack": { + "version": "5.80.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.80.0.tgz", + "integrity": "sha512-OIMiq37XK1rWO8mH9ssfFKZsXg4n6klTEDL7S8/HqbAOBBaiy8ABvXvz0dDCXeEF9gqwxSvVk611zFPjS8hJxA==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.13.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "dependencies": { + "acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true + }, + "acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", + "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "webpack-bundle-analyzer": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.8.0.tgz", + "integrity": "sha512-ZzoSBePshOKhr+hd8u6oCkZVwpVaXgpw23ScGLFpR6SjYI7+7iIWYarjN6OEYOfRt8o7ZyZZQk0DuMizJ+LEIg==", + "dev": true, + "requires": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "gzip-size": "^6.0.0", + "lodash": "^4.17.20", + "opener": "^1.5.2", + "sirv": "^1.0.7", + "ws": "^7.3.1" + }, + "dependencies": { + "acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "dev": true + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "ws": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", + "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==", + "dev": true, + "requires": {} + } + } + }, + "webpack-dev-middleware": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.0.2.tgz", + "integrity": "sha512-iOddiJzPcQC6lwOIu60vscbGWth8PCRcWRCwoQcTQf9RMoOWBHg5EyzpGdtSmGMrSPd5vHEfFXmVErQEmkRngQ==", + "dev": true, + "requires": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + } + }, + "webpack-dev-server": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.2.tgz", + "integrity": "sha512-5i6TrGBRxG4vnfDpB6qSQGfnB6skGBXNL5/542w2uRGLimX6qeE5BQMLrzIC3JYV/xlGOv+s+hTleI9AZKUQNw==", + "dev": true, + "requires": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.1", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "dependencies": { + "webpack-dev-middleware": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", + "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "dev": true, + "requires": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + } + }, + "ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "requires": {} + } + } + }, + "webpack-ext-reloader": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/webpack-ext-reloader/-/webpack-ext-reloader-1.1.9.tgz", + "integrity": "sha512-6AVXGrjcVHKtIQn4yGGghJpiIV2h9F7hNKLsh1oP8m+d6H3QLF3jTNu3vNdKu/8Lab3J/gwb7Bm7tjZLa+DS6g==", + "dev": true, + "requires": { + "@types/webextension-polyfill": "^0.8.2", + "@types/webpack": "^5.28.0", + "@types/webpack-sources": "^3.2.0", + "clean-webpack-plugin": "^4.0.0", + "colors": "^1.4.0", + "cross-env": "^7.0.3", + "lodash": "^4.17.21", + "minimist": "^1.2.5", + "useragent": "^2.3.0", + "webextension-polyfill": "^0.8.0", + "webpack-sources": "^3.2.3", + "ws": "^8.4.2" + } + }, + "webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true + }, + "webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "requires": { + "typed-assert": "^1.0.8" + } + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true + }, + "whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "requires": { + "iconv-lite": "0.6.3" + } + }, + "whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==" + }, + "whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "requires": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "requires": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "which-typed-array": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz", + "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.6", + "call-bind": "^1.0.5", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.1" + } + }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + } + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "requires": {} + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true + }, + "xxhashjs": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz", + "integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==", + "dev": true, + "requires": { + "cuint": "^0.2.2" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yaml": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "dependencies": { + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + } + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + }, + "zip-a-folder": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/zip-a-folder/-/zip-a-folder-1.1.5.tgz", + "integrity": "sha512-w6I4mvWc6D0Q4pdzCSFbQih/ezYBdjwGZVbWRRFMOYcOdtE9TONZ7YtXCPnHj4XJQmXQxTOWcRGnPYxRn+d0mw==", + "dev": true, + "requires": { + "archiver": "^5.3.1" + } + }, + "zip-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz", + "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==", + "dev": true, + "requires": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.1.0", + "readable-stream": "^3.6.0" + } + }, + "zone.js": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.13.0.tgz", + "integrity": "sha512-7m3hNNyswsdoDobCkYNAy5WiUulkMd3+fWaGT9ij6iq3Zr/IwJo4RMCYPSDjT+r7tnPErmY9sZpKhWQ8S5k6XQ==", + "requires": { + "tslib": "^2.3.0" + } + } + } +} diff --git a/desktop/angular/package.json b/desktop/angular/package.json new file mode 100644 index 00000000..4131e18f --- /dev/null +++ b/desktop/angular/package.json @@ -0,0 +1,105 @@ +{ + "name": "portmaster", + "version": "0.8.3", + "scripts": { + "ng": "ng", + "start": "npm install && npm run build-libs:dev && ng serve --proxy-config ./proxy.json", + "build-libs": "NODE_ENV=production ng build --configuration production @safing/ui && NODE_ENV=production ng build --configuration production @safing/portmaster-api", + "build-libs:dev": "ng build --configuration development @safing/ui && ng build --configuration development @safing/portmaster-api", + "serve": "npm run build-libs:dev && ng serve --proxy-config ./proxy.json", + "build:dev": "npm run build-libs:dev && ng build", + "test": "ng test", + "lint": "ng lint", + "e2e": "ng e2e", + "zip-dist": "node pack.js", + "chrome-extension": "NODE_ENV=production ng build --configuration production portmaster-chrome-extension", + "chrome-extension:dev": "ng build --configuration development portmaster-chrome-extension --watch", + "build": "npm run build-libs && NODE_ENV=production ng build --configuration production --base-href /ui/modules/portmaster/", + "build-tauri": "npm run build-libs && NODE_ENV=production ng build --configuration production", + "serve-tauri-builtin": "ng serve tauri-builtin --port 4100", + "serve-app": "ng serve --port 4200 --proxy-config ./proxy.json", + "tauri-dev": "npm install && run-s build-libs:dev && run-p serve-app serve-tauri-builtin" + }, + "private": true, + "dependencies": { + "@angular/animations": "^16.0.1", + "@angular/cdk": "^16.0.1", + "@angular/common": "^16.0.1", + "@angular/compiler": "^16.0.1", + "@angular/core": "^16.0.1", + "@angular/forms": "^16.0.1", + "@angular/localize": "^16.0.1", + "@angular/platform-browser": "^16.0.1", + "@angular/platform-browser-dynamic": "^16.0.1", + "@angular/router": "^16.0.1", + "@fortawesome/angular-fontawesome": "^0.13.0", + "@fortawesome/fontawesome-svg-core": "^6.4.0", + "@fortawesome/free-brands-svg-icons": "^6.4.0", + "@fortawesome/free-regular-svg-icons": "^6.4.0", + "@fortawesome/free-solid-svg-icons": "^6.4.0", + "@tauri-apps/api": "^2.0.0-beta.3", + "@tauri-apps/plugin-cli": "^2.0.0-beta.1", + "@tauri-apps/plugin-clipboard-manager": "^2.0.0-alpha.4", + "@tauri-apps/plugin-dialog": "^2.0.0-alpha.4", + "@tauri-apps/plugin-notification": "^2.0.0-alpha.4", + "@tauri-apps/plugin-os": "^2.0.0-alpha.5", + "@tauri-apps/plugin-shell": "^2.0.0-alpha.4", + "autoprefixer": "^10.4.14", + "d3": "^7.8.4", + "data-urls": "^5.0.0", + "emoji-toolkit": "^7.0.1", + "fuse.js": "^6.6.2", + "ng-zorro-antd": "^16.1.0", + "ngx-markdown": "^16.0.0", + "postcss": "^8.4.23", + "prismjs": "^1.29.0", + "psl": "^1.9.0", + "rxjs": "~7.8.1", + "topojson-client": "^3.1.0", + "topojson-simplify": "^3.0.3", + "tslib": "^2.5.0", + "whatwg-encoding": "^3.1.1", + "zone.js": "^0.13.0" + }, + "devDependencies": { + "@angular-builders/custom-webpack": "^16.0.0-beta.1", + "@angular-devkit/build-angular": "^16.0.1", + "@angular-eslint/builder": "16.0.1", + "@angular-eslint/eslint-plugin": "16.0.1", + "@angular-eslint/eslint-plugin-template": "16.0.1", + "@angular-eslint/schematics": "16.0.1", + "@angular-eslint/template-parser": "16.0.1", + "@angular/cli": "^16.0.1", + "@angular/compiler-cli": "^16.0.1", + "@fullhuman/postcss-purgecss": "^5.0.0", + "@types/chrome": "^0.0.236", + "@types/d3": "^7.4.0", + "@types/data-urls": "^3.0.4", + "@types/jasmine": "^4.3.1", + "@types/jasminewd2": "~2.0.10", + "@types/node": "^20.1.5", + "@types/psl": "^1.1.0", + "@types/topojson-client": "^3.1.1", + "@types/topojson-simplify": "^3.0.1", + "@types/whatwg-encoding": "^2.0.3", + "@typescript-eslint/eslint-plugin": "^5.59.6", + "@typescript-eslint/parser": "^5.59.6", + "eslint": "^8.40.0", + "jasmine-core": "^5.0.0", + "jasmine-spec-reporter": "^7.0.0", + "js-yaml-loader": "^1.2.2", + "ng-packagr": "^16.0.1", + "npm-run-all": "^4.1.5", + "postcss-import": "^15.1.0", + "postcss-loader": "^7.3.0", + "postcss-scss": "^4.0.6", + "protractor": "~7.0.0", + "tailwindcss": "^3.3.2", + "ts-node": "^10.9.1", + "tslint": "~6.1.0", + "typescript": "4.9", + "webpack-bundle-analyzer": "^4.8.0", + "webpack-ext-reloader": "^1.1.9", + "zip-a-folder": "^1.1.5" + } +} diff --git a/desktop/angular/projects/portmaster-chrome-extension/karma.conf.js b/desktop/angular/projects/portmaster-chrome-extension/karma.conf.js new file mode 100644 index 00000000..eaac9a49 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, '../../coverage/portmaster-chrome-extension'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/app-routing.module.ts b/desktop/angular/projects/portmaster-chrome-extension/src/app/app-routing.module.ts new file mode 100644 index 00000000..73a41af9 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/app-routing.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { ExtDomainListComponent } from './domain-list'; +import { IntroComponent } from './welcome/intro.component'; + +const routes: Routes = [ + { path: '', pathMatch: 'full', component: ExtDomainListComponent }, + { path: 'authorize', pathMatch: 'prefix', component: IntroComponent } +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] +}) +export class AppRoutingModule { } diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/app.component.html b/desktop/angular/projects/portmaster-chrome-extension/src/app/app.component.html new file mode 100644 index 00000000..d1b1eb54 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/app.component.html @@ -0,0 +1,3 @@ + + + diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/app.component.scss b/desktop/angular/projects/portmaster-chrome-extension/src/app/app.component.scss new file mode 100644 index 00000000..b25b9d22 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/app.component.scss @@ -0,0 +1,3 @@ +:host { + @apply bg-background text-white flex flex-col w-96 h-96; +} diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/app.component.ts b/desktop/angular/projects/portmaster-chrome-extension/src/app/app.component.ts new file mode 100644 index 00000000..e8d9a987 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/app.component.ts @@ -0,0 +1,54 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { MetaAPI, MyProfileResponse, retryPipeline } from '@safing/portmaster-api'; +import { catchError, filter, throwError } from 'rxjs'; + + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], +}) +export class AppComponent implements OnInit { + isAuthorizeView = false; + + constructor( + private metaapi: MetaAPI, + private router: Router, + ) { } + + profile: MyProfileResponse | null = null; + + ngOnInit(): void { + this.router.events + .pipe( + filter(event => event instanceof NavigationEnd) + ) + .subscribe(event => { + if (event instanceof NavigationEnd) { + this.isAuthorizeView = event.url.includes("/authorize") + } + }) + + this.metaapi.myProfile() + .pipe( + catchError(err => { + if (err instanceof HttpErrorResponse && err.status === 403) { + this.router.navigate(['/authorize']) + } + + return throwError(() => err) + }), + retryPipeline() + ) + .subscribe({ + next: profile => { + this.profile = profile; + + console.log(this.profile); + } + }) + } + +} diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/app.module.ts b/desktop/angular/projects/portmaster-chrome-extension/src/app/app.module.ts new file mode 100644 index 00000000..93c418a3 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/app.module.ts @@ -0,0 +1,39 @@ +import { OverlayModule } from '@angular/cdk/overlay'; +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { PortmasterAPIModule } from '@safing/portmaster-api'; +import { TabModule } from '@safing/ui'; +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { ExtDomainListComponent } from './domain-list'; +import { ExtHeaderComponent } from './header'; +import { AuthIntercepter as AuthInterceptor } from './interceptor'; +import { WelcomeModule } from './welcome'; + + +@NgModule({ + declarations: [ + AppComponent, + ExtDomainListComponent, + ExtHeaderComponent, + ], + imports: [ + BrowserModule, + AppRoutingModule, + HttpClientModule, + PortmasterAPIModule.forRoot(), + TabModule, + WelcomeModule, + OverlayModule, + ], + providers: [ + { + provide: HTTP_INTERCEPTORS, + multi: true, + useClass: AuthInterceptor, + } + ], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/domain-list/domain-list.component.html b/desktop/angular/projects/portmaster-chrome-extension/src/app/domain-list/domain-list.component.html new file mode 100644 index 00000000..44bc3f02 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/domain-list/domain-list.component.html @@ -0,0 +1,27 @@ +

diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/domain-list/domain-list.component.ts b/desktop/angular/projects/portmaster-chrome-extension/src/app/domain-list/domain-list.component.ts new file mode 100644 index 00000000..b0e78d45 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/domain-list/domain-list.component.ts @@ -0,0 +1,129 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from "@angular/core"; +import { Netquery, NetqueryConnection } from "@safing/portmaster-api"; +import { ListRequests, NotifyRequests } from "../../background/commands"; +import { Request } from '../../background/tab-tracker'; + +interface DomainRequests { + domain: string; + requests: Request[]; + latestIsBlocked: boolean; + lastConn?: NetqueryConnection; +} + +@Component({ + selector: 'ext-domain-list', + templateUrl: './domain-list.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styles: [ + ` + :host { + @apply flex flex-grow flex-col overflow-auto; + } + ` + ] +}) +export class ExtDomainListComponent implements OnInit { + requests: DomainRequests[] = []; + + constructor( + private netquery: Netquery, + private cdr: ChangeDetectorRef + ) { } + + ngOnInit() { + // setup listening for requests sent from our background script + const self = this; + chrome.runtime.onMessage.addListener((msg: NotifyRequests) => { + if (typeof msg !== 'object') { + console.error('Received invalid message from background script') + + return; + } + + console.log(`DEBUG: received command ${msg.type} from background script`) + + switch (msg.type) { + case 'notifyRequests': + self.updateRequests(msg.requests); + break; + + default: + console.error('Received unknown command from background script') + } + }) + + this.loadRequests(); + } + + updateRequests(req: Request[]) { + let m = new Map(); + + this.requests.forEach(obj => { + obj.requests = []; + m.set(obj.domain, obj); + }); + + req.forEach(r => { + let obj = m.get(r.domain); + if (!obj) { + obj = { + domain: r.domain, + requests: [], + latestIsBlocked: false + } + m.set(r.domain, obj) + } + + obj.requests.push(r); + }) + + this.requests = []; + Array.from(m.keys()).sort() + .map(key => m.get(key)!) + .forEach(obj => { + this.requests.push(obj) + + this.netquery.query({ + query: { + domain: obj.domain, + }, + orderBy: [ + { + field: 'started', + desc: true, + } + ], + page: 0, + pageSize: 1, + }) + .subscribe(result => { + if (!result[0]) { + return; + } + + obj.latestIsBlocked = !result[0].allowed; + obj.lastConn = result[0] as NetqueryConnection; + }) + }) + + this.cdr.detectChanges(); + } + + private loadRequests() { + const cmd: ListRequests = { + type: 'listRequests', + tabId: 'current' + } + + const self = this; + chrome.runtime.sendMessage(cmd, (response: any) => { + if (Array.isArray(response)) { + self.updateRequests(response) + + return; + } + + console.error(response); + }) + } +} diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/domain-list/index.ts b/desktop/angular/projects/portmaster-chrome-extension/src/app/domain-list/index.ts new file mode 100644 index 00000000..c0b4110c --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/domain-list/index.ts @@ -0,0 +1 @@ +export * from './domain-list.component'; diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/header/header.component.html b/desktop/angular/projects/portmaster-chrome-extension/src/app/header/header.component.html new file mode 100644 index 00000000..e61fb0e7 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/header/header.component.html @@ -0,0 +1,22 @@ +
+ + + + + + + + + + + + Secure + +
diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/header/header.component.scss b/desktop/angular/projects/portmaster-chrome-extension/src/app/header/header.component.scss new file mode 100644 index 00000000..5c958f4e --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/header/header.component.scss @@ -0,0 +1,29 @@ +svg { + transform: scale(0.95); + + path { + top: 0px; + left: 0px; + transform-origin: center center; + } + + .shield-one { + transform: scale(.62); + } + + .shield-two { + animation-delay: -1.2s; + opacity: .6; + transform: scale(.8); + } + + .shield-three { + animation-delay: -2.5s; + opacity: .4; + transform: scale(1); + } + + .shield-ok { + transform: scale(.62); + } +} diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/header/header.component.ts b/desktop/angular/projects/portmaster-chrome-extension/src/app/header/header.component.ts new file mode 100644 index 00000000..3712f321 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/header/header.component.ts @@ -0,0 +1,9 @@ +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +@Component({ + selector: 'ext-header', + templateUrl: './header.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./header.component.scss'] +}) +export class ExtHeaderComponent { } diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/header/index.ts b/desktop/angular/projects/portmaster-chrome-extension/src/app/header/index.ts new file mode 100644 index 00000000..be62c26c --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/header/index.ts @@ -0,0 +1 @@ +export * from './header.component'; diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/interceptor.ts b/desktop/angular/projects/portmaster-chrome-extension/src/app/interceptor.ts new file mode 100644 index 00000000..a33e1d04 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/interceptor.ts @@ -0,0 +1,45 @@ +import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { BehaviorSubject, filter, Observable, switchMap } from "rxjs"; + + +@Injectable() +export class AuthIntercepter implements HttpInterceptor { + /** Used to delay requests until we loaded the access token from the extension storage. */ + private loaded$ = new BehaviorSubject(false); + + /** Holds the access token required to talk to the Portmaster API. */ + private token: string | null = null; + + constructor() { + // make sure we use the new access token once we get one. + chrome.storage.onChanged.addListener(changes => { + this.token = changes['key'].newValue || null; + }) + + // try to read the current access token from the extension storage. + chrome.storage.local.get('key', obj => { + this.token = obj.key || null; + console.log("got token", this.token) + this.loaded$.next(true); + }) + + chrome.runtime.sendMessage({ type: 'listRequests', tabId: 'current' }, (response: any) => { + console.log(response); + }) + } + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + return this.loaded$.pipe( + filter(loaded => loaded), + switchMap(() => { + if (!!this.token) { + req = req.clone({ + headers: req.headers.set("Authorization", "Bearer " + this.token) + }) + } + return next.handle(req) + }) + ) + } +} diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/request-interceptor.service.ts b/desktop/angular/projects/portmaster-chrome-extension/src/app/request-interceptor.service.ts new file mode 100644 index 00000000..159a5ea5 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/request-interceptor.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from "@angular/core"; +import { Subject } from "rxjs"; + + + +@Injectable({ + providedIn: 'root' +}) +export class RequestInterceptorService { + /** Used to emit when a new URL was requested */ + private onUrlRequested$ = new Subject(); + + /** Used to emit when a URL has likely been blocked by the portmaster */ + private onUrlBlocked$ = new Subject(); + + /** Emits when a new URL was requested */ + get onUrlRequested() { + return this.onUrlRequested$.asObservable(); + } + + /** Emits when a new URL was likely blocked by the portmaster */ + get onUrlBlocked() { + return this.onUrlBlocked$.asObservable(); + } + + constructor() { + this.registerCallbacks() + } + + private registerCallbacks() { + const filter = { + urls: [ + "http://*/*", + "https://*/*", + ] + }; + + chrome.webRequest.onBeforeRequest.addListener(details => this.onUrlRequested$.next(details), filter) + chrome.webRequest.onErrorOccurred.addListener(details => { + if (details.error !== "net::ERR_ADDRESS_UNREACHABLE") { + // we don't care about errors other than UNREACHABLE because that's error caused + // by the portmaster. + return; + } + + this.onUrlBlocked$.next(details); + }, filter) + } +} diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/welcome/index.ts b/desktop/angular/projects/portmaster-chrome-extension/src/app/welcome/index.ts new file mode 100644 index 00000000..a695cb02 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/welcome/index.ts @@ -0,0 +1,2 @@ +export * from './welcome.module'; + diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/welcome/intro.component.html b/desktop/angular/projects/portmaster-chrome-extension/src/app/welcome/intro.component.html new file mode 100644 index 00000000..017da699 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/welcome/intro.component.html @@ -0,0 +1,48 @@ +
+ +

+ + + + + + + + + + + Welcome to the + + Portmaster Browser Extension + + +

+
+
+ + + This extension adds direct support for Portmaster to your Browser. For that, it needs to get access to the + Portmaster on your system. For security reasons, you first need to authorize the Browser Extension to talk to the + Portmaster. + + + + + +

Waiting for Authorization

+ + Please open the Portmaster and approve the authorization request. + +
+ + +
diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/welcome/intro.component.ts b/desktop/angular/projects/portmaster-chrome-extension/src/app/welcome/intro.component.ts new file mode 100644 index 00000000..45d6b3d9 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/welcome/intro.component.ts @@ -0,0 +1,44 @@ +import { Component } from "@angular/core"; +import { Router } from "@angular/router"; +import { MetaAPI } from "@safing/portmaster-api"; +import { Subject, takeUntil } from "rxjs"; + +@Component({ + templateUrl: './intro.component.html', + styles: [ + ` + :host { + @apply flex flex-col h-full; + } + ` + ] +}) +export class IntroComponent { + private cancelRequest$ = new Subject(); + + state: 'authorizing' | 'failed' | '' = ''; + + constructor( + private meta: MetaAPI, + private router: Router, + ) { } + + authorizeExtension() { + // cancel any pending request + this.cancelRequest$.next(); + + this.state = 'authorizing'; + this.meta.requestApplicationAccess("Portmaster Browser Extension") + .pipe(takeUntil(this.cancelRequest$)) + .subscribe({ + next: token => { + chrome.storage.local.set(token); + console.log(token); + this.router.navigate(['/']) + }, + error: err => { + this.state = 'failed'; + } + }) + } +} diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/app/welcome/welcome.module.ts b/desktop/angular/projects/portmaster-chrome-extension/src/app/welcome/welcome.module.ts new file mode 100644 index 00000000..a0de7207 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/app/welcome/welcome.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { OverlayStepperModule } from "@safing/ui"; +import { IntroComponent } from "./intro.component"; + +@NgModule({ + imports: [ + CommonModule, + OverlayStepperModule, + ], + declarations: [ + IntroComponent, + ], + exports: [ + IntroComponent, + ] +}) +export class WelcomeModule { } + diff --git a/assets/.gitkeep b/desktop/angular/projects/portmaster-chrome-extension/src/assets/.gitkeep similarity index 100% rename from assets/.gitkeep rename to desktop/angular/projects/portmaster-chrome-extension/src/assets/.gitkeep diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/assets/icon_128.png b/desktop/angular/projects/portmaster-chrome-extension/src/assets/icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..063948f1789284627332181127d006f59e99d9b3 GIT binary patch literal 11328 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 literal 0 HcmV?d00001 diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/background.ts b/desktop/angular/projects/portmaster-chrome-extension/src/background.ts new file mode 100644 index 00000000..e6a0986c --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/background.ts @@ -0,0 +1,133 @@ +import { debounceTime, Subject } from "rxjs"; +import { CallRequest, ListRequests, NotifyRequests } from "./background/commands"; +import { Request, TabTracker } from "./background/tab-tracker"; +import { getCurrentTab } from "./background/tab-utils"; + +export class BackgroundService { + /** a lookup map for tab trackers by tab-id */ + private trackers = new Map(); + + /** used to signal the pop-up that new requests arrived */ + private notifyRequests = new Subject(); + + constructor() { + // register a navigation-completed listener. This is fired when the user switches to a new website + // by entering it in the browser address bar. + chrome.webNavigation.onCompleted.addListener((details) => { + console.log("event: webNavigation.onCompleted", details); + }) + + // request event listeners for new requests and errors that occured for them. + // We only care about http and https here. + const filter = { + urls: [ + 'http://*/*', + 'https://*/*' + ] + } + chrome.webRequest.onBeforeRequest.addListener(details => this.handleOnBeforeRequest(details), filter) + chrome.webRequest.onErrorOccurred.addListener(details => this.handleOnErrorOccured(details), filter) + + // make sure we can communicate with the extension popup + chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => this.handleMessage(msg, sender, sendResponse)) + + // set-up signalling of new requests to the pop-up + this.notifyRequests + .pipe(debounceTime(500)) + .subscribe(async () => { + const currentTab = await getCurrentTab(); + if (!!currentTab && !!currentTab.id) { + const msg: NotifyRequests = { + type: 'notifyRequests', + requests: this.mustGetTab({ tabId: currentTab.id }).allRequests() + } + + chrome.runtime.sendMessage(msg) + } + }) + } + + /** Callback for messages sent by the popup */ + private handleMessage(msg: CallRequest, sender: chrome.runtime.MessageSender, sendResponse: (msg: any) => void) { + console.log(`DEBUG: got message from ${sender.origin} (tab=${sender.tab?.id})`) + + if (typeof msg !== 'object') { + console.error(`Received invalid message from popup`, msg) + + return; + } + + let response: Promise; + switch (msg.type) { + case 'listRequests': + response = this.handleListRequests(msg) + break; + + default: + response = Promise.reject("unknown command") + } + + response + .then(res => { + console.log(`DEBUG: sending response for command ${msg.type}`, res) + sendResponse(res); + }) + .catch(err => { + console.error(`Failed to handle command ${msg.type}`, err) + sendResponse({ + type: 'error', + details: err + }); + }) + } + + /** Returns a list of all observed requests based on the filter in msg. */ + private async handleListRequests(msg: ListRequests): Promise { + if (msg.tabId === 'current') { + const currentID = (await getCurrentTab()).id + if (!currentID) { + return []; + } + + msg.tabId = currentID; + } + + const tracker = this.mustGetTab({ tabId: msg.tabId as number }) + + if (!!msg.domain) { + return tracker.forDomain(msg.domain) + } + + return tracker.allRequests() + } + + /** Callback for chrome.webRequest.onBeforeRequest */ + private handleOnBeforeRequest(details: chrome.webRequest.WebRequestDetails) { + this.mustGetTab(details).trackRequest(details) + + this.notifyRequests.next(); + } + + /** Callback for chrome.webRequest.onErrorOccured */ + private handleOnErrorOccured(details: chrome.webRequest.WebResponseErrorDetails) { + this.mustGetTab(details).trackError(details); + + this.notifyRequests.next(); + } + + /** Returns the tab-tracker for tabId. Creates a new tracker if none exists. */ + private mustGetTab({ tabId }: { tabId: number }): TabTracker { + let tracker = this.trackers.get(tabId); + if (!tracker) { + tracker = new TabTracker(tabId) + this.trackers.set(tabId, tracker) + } + + return tracker; + } +} + +/** start the background service once we got successfully installed. */ +chrome.runtime.onInstalled.addListener(() => { + new BackgroundService() +}); diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/background/commands.ts b/desktop/angular/projects/portmaster-chrome-extension/src/background/commands.ts new file mode 100644 index 00000000..6bfdcd88 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/background/commands.ts @@ -0,0 +1,14 @@ +import { Request } from "./tab-tracker"; + +export interface ListRequests { + type: 'listRequests'; + domain?: string; + tabId: number | 'current'; +} + +export interface NotifyRequests { + type: 'notifyRequests', + requests: Request[]; +} + +export type CallRequest = ListRequests; diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/background/tab-tracker.ts b/desktop/angular/projects/portmaster-chrome-extension/src/background/tab-tracker.ts new file mode 100644 index 00000000..f5a0628e --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/background/tab-tracker.ts @@ -0,0 +1,126 @@ +import { deepClone } from "@safing/portmaster-api"; + +export interface Request { + /** The ID assigned by the browser */ + id: string; + + /** The domain this request was for */ + domain: string; + + /** The timestamp in milliseconds since epoch at which the request was initiated */ + time: number; + + /** Whether or not this request errored with net::ERR_ADDRESS_UNREACHABLE */ + isUnreachable: boolean; +} + +/** + * TabTracker tracks requests to domains made by a single browser tab. + */ +export class TabTracker { + /** A list of requests observed for this tab order by time they have been initiated */ + private requests: Request[] = []; + + /** A lookup map for requests to specific domains */ + private byDomain = new Map(); + + /** A lookup map for requests by the chrome request ID */ + private byRequestId = new Map; + + constructor(public readonly tabId: number) { } + + /** Returns an array of all requests observed in this tab. */ + allRequests(): Request[] { + return deepClone(this.requests) + } + + /** Returns a list of requests that have been observed for domain */ + forDomain(domain: string): Request[] { + if (!domain.endsWith(".")) { + domain += "." + } + + return this.byDomain.get(domain) || []; + } + + /** Call to add the details of a web-request to this tab-tracker */ + trackRequest(details: chrome.webRequest.WebRequestDetails) { + // If this is the wrong tab ID ignore the request details + if (details.tabId !== this.tabId) { + console.error(`TabTracker.trackRequest: called with wrong tab ID. Expected ${this.tabId} but got ${details.tabId}`) + + return; + } + + // if the type of the request is for the main_frame the user switched to a new website. + // In that case, we can wipe out all currently stored requests as the user will likely not + // care anymore. + if (details.type === "main_frame") { + this.clearState(); + } + + // get the domain of the request normalized to contain the trailing dot. + let domain = new URL(details.url).host; + if (!domain.endsWith(".")) { + domain += "." + } + + const req: Request = { + id: details.requestId, + domain: domain, + time: details.timeStamp, + isUnreachable: false, // we don't actually know that yet + } + + this.requests.push(req); + this.byRequestId.set(req.id, req) + + // Add the request to the by-domain lookup map + let byDomainRequests = this.byDomain.get(req.domain); + if (!byDomainRequests) { + byDomainRequests = []; + this.byDomain.set(req.domain, byDomainRequests) + } + byDomainRequests.push(req) + + console.log(`DEBUG: observed request ${req.id} to ${req.domain}`) + } + + /** Call to notify the tab-tracker of a request error */ + trackError(errorDetails: chrome.webRequest.WebResponseErrorDetails) { + // we only care about net::ERR_ADDRESS_UNREACHABLE here because that's how the + // Portmaster blocks the request. + + // TODO(ppacher): docs say we must not rely on that value so we should figure out a better + // way to detect if the error is caused by the Portmaster. + if (errorDetails.error !== "net::ERR_ADDRESS_UNREACHABLE") { + return; + } + + // the the previsouly observed request by the request ID. + const req = this.byRequestId.get(errorDetails.requestId) + if (!req) { + console.error("TabTracker.trackError: request has not been observed before") + + return + } + + // make sure the error details actually happend for the observed tab. + if (errorDetails.tabId !== this.tabId) { + console.error(`TabTracker.trackRequest: called with wrong tab ID. Expected ${this.tabId} but got ${errorDetails.tabId}`) + + return; + } + + // mark the request as unreachable. + req.isUnreachable = true; + console.log(`DEBUG: marked request ${req.id} to ${req.domain} as unreachable`) + } + + /** Clears the current state of the tab tracker */ + private clearState() { + this.requests = []; + this.byDomain = new Map(); + this.byRequestId = new Map(); + } +} diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/background/tab-utils.ts b/desktop/angular/projects/portmaster-chrome-extension/src/background/tab-utils.ts new file mode 100644 index 00000000..36635ca8 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/background/tab-utils.ts @@ -0,0 +1,9 @@ + +/** Queries and returns the currently active tab */ +export function getCurrentTab(): Promise { + return new Promise((resolve) => { + chrome.tabs.query({ active: true, lastFocusedWindow: true }, ([tab]) => { + resolve(tab); + }) + }) +} diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/environments/environment.prod.ts b/desktop/angular/projects/portmaster-chrome-extension/src/environments/environment.prod.ts new file mode 100644 index 00000000..ffe8aed7 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: false +}; diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/environments/environment.ts b/desktop/angular/projects/portmaster-chrome-extension/src/environments/environment.ts new file mode 100644 index 00000000..f56ff470 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/environments/environment.ts @@ -0,0 +1,16 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/favicon.ico b/desktop/angular/projects/portmaster-chrome-extension/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..997406ad22c29aae95893fb3d666c30258a09537 GIT binary patch literal 948 zcmV;l155mgP)CBYU7IjCFmI-B}4sMJt3^s9NVg!P0 z6hDQy(L`XWMkB@zOLgN$4KYz;j0zZxq9KKdpZE#5@k0crP^5f9KO};h)ZDQ%ybhht z%t9#h|nu0K(bJ ztIkhEr!*UyrZWQ1k2+YkGqDi8Z<|mIN&$kzpKl{cNP=OQzXHz>vn+c)F)zO|Bou>E z2|-d_=qY#Y+yOu1a}XI?cU}%04)zz%anD(XZC{#~WreV!a$7k2Ug`?&CUEc0EtrkZ zL49MB)h!_K{H(*l_93D5tO0;BUnvYlo+;yss%n^&qjt6fZOa+}+FDO(~2>G z2dx@=JZ?DHP^;b7*Y1as5^uphBsh*s*z&MBd?e@I>-9kU>63PjP&^#5YTOb&x^6Cf z?674rmSHB5Fk!{Gv7rv!?qX#ei_L(XtwVqLX3L}$MI|kJ*w(rhx~tc&L&xP#?cQow zX_|gx$wMr3pRZIIr_;;O|8fAjd;1`nOeu5K(pCu7>^3E&D2OBBq?sYa(%S?GwG&_0-s%_v$L@R!5H_fc)lOb9ZoOO#p`Nn`KU z3LTTBtjwo`7(HA6 z7gmO$yTR!5L>Bsg!X8616{JUngg_@&85%>W=mChTR;x4`P=?PJ~oPuy5 zU-L`C@_!34D21{fD~Y8NVnR3t;aqZI3fIhmgmx}$oc-dKDC6Ap$Gy>a!`A*x2L1v0 WcZ@i?LyX}70000 + + + + PortmasterChromeExtension + + + + + + + + diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/main.ts b/desktop/angular/projects/portmaster-chrome-extension/src/main.ts new file mode 100644 index 00000000..c7b673cf --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/manifest.json b/desktop/angular/projects/portmaster-chrome-extension/src/manifest.json new file mode 100644 index 00000000..db045a05 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "Portmaster Browser Extension", + "version": "0.1", + "description": "Browser Extension for even better Portmaster integration", + "manifest_version": 2, + "permissions": [ + "activeTab", + "storage", + "webRequest", + "webNavigation", + "*://*/*" + ], + "browser_action": { + "default_popup": "index.html", + "default_icon": { + "128": "assets/icon_128.png" + } + }, + "background": { + "scripts": ["background.js"], + "persistent": true + } +} diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/polyfills.ts b/desktop/angular/projects/portmaster-chrome-extension/src/polyfills.ts new file mode 100644 index 00000000..429bb9ef --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/polyfills.ts @@ -0,0 +1,53 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes recent versions of Safari, Chrome (including + * Opera), Edge on the desktop, and iOS and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js'; // Included with Angular CLI. + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/styles.scss b/desktop/angular/projects/portmaster-chrome-extension/src/styles.scss new file mode 100644 index 00000000..e41283cd --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/styles.scss @@ -0,0 +1,8 @@ +/* You can add global styles to this file, and also import other style files */ + +@import 'tailwindcss/base'; +@import 'tailwindcss/components'; +@import 'tailwindcss/utilities'; + + +@import '@angular/cdk/overlay-prebuilt'; diff --git a/desktop/angular/projects/portmaster-chrome-extension/src/test.ts b/desktop/angular/projects/portmaster-chrome-extension/src/test.ts new file mode 100644 index 00000000..51bb0206 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/src/test.ts @@ -0,0 +1,14 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), +); diff --git a/desktop/angular/projects/portmaster-chrome-extension/tsconfig.app.json b/desktop/angular/projects/portmaster-chrome-extension/tsconfig.app.json new file mode 100644 index 00000000..28c28154 --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/tsconfig.app.json @@ -0,0 +1,18 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/app", + "types": [ + "chrome" + ] + }, + "files": [ + "src/main.ts", + "src/polyfills.ts", + "src/background.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/desktop/angular/projects/portmaster-chrome-extension/tsconfig.spec.json b/desktop/angular/projects/portmaster-chrome-extension/tsconfig.spec.json new file mode 100644 index 00000000..b66a2f0b --- /dev/null +++ b/desktop/angular/projects/portmaster-chrome-extension/tsconfig.spec.json @@ -0,0 +1,18 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/desktop/angular/projects/safing/portmaster-api/README.md b/desktop/angular/projects/safing/portmaster-api/README.md new file mode 100644 index 00000000..fc4110d2 --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/README.md @@ -0,0 +1,24 @@ +# PortmasterApi + +This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.0. + +## Code scaffolding + +Run `ng generate component component-name --project portmaster-api` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project portmaster-api`. +> Note: Don't forget to add `--project portmaster-api` or else it will be added to the default project in your `angular.json` file. + +## Build + +Run `ng build portmaster-api` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Publishing + +After building your library with `ng build portmaster-api`, go to the dist folder `cd dist/portmaster-api` and run `npm publish`. + +## Running unit tests + +Run `ng test portmaster-api` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/desktop/angular/projects/safing/portmaster-api/karma.conf.js b/desktop/angular/projects/safing/portmaster-api/karma.conf.js new file mode 100644 index 00000000..6f9bd935 --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, '../../../coverage/safing/portmaster-api'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/desktop/angular/projects/safing/portmaster-api/ng-package.json b/desktop/angular/projects/safing/portmaster-api/ng-package.json new file mode 100644 index 00000000..4ea94f9a --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist-lib/safing/portmaster-api", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/desktop/angular/projects/safing/portmaster-api/package-lock.json b/desktop/angular/projects/safing/portmaster-api/package-lock.json new file mode 100644 index 00000000..848065cc --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/package-lock.json @@ -0,0 +1,132 @@ +{ + "name": "@safing/portmaster-api", + "version": "0.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@safing/portmaster-api", + "version": "0.0.1", + "dependencies": { + "tslib": "^2.3.0" + }, + "devDependencies": { + "@types/jasmine": "^4.0.3" + }, + "peerDependencies": { + "@angular/common": "^14.0.0", + "@angular/core": "^14.0.0" + } + }, + "node_modules/@angular/common": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-14.0.5.tgz", + "integrity": "sha512-YFRPxx3yRLjk0gPL7tm/97mi8+Pjt3q6zWCjrLkAlDjniDvgmKNWIQ1h6crZQR0Cw7yNqK0QoFXQgTw0GJIWLQ==", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/core": "14.0.5", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/core": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-14.0.5.tgz", + "integrity": "sha512-4MIfFM2nD+N0/Dk8xKfKvbdS/zYRhQgdnKT6ZIIV7Y/XCfn5QAIa4+vB5BEAZpuzSsZHLVdBQQ0TkaiONLfL2Q==", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.11.4" + } + }, + "node_modules/@types/jasmine": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.0.3.tgz", + "integrity": "sha512-Opp1LvvEuZdk8fSSvchK2mZwhVrsNT0JgJE9Di6MjnaIpmEXM8TLCPPrVtNTYh8+5MPdY8j9bAHMu2SSfwpZJg==", + "dev": true + }, + "node_modules/rxjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz", + "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "node_modules/zone.js": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.6.tgz", + "integrity": "sha512-umJqFtKyZlPli669gB1gOrRE9hxUUGkZr7mo878z+NEBJZZixJkKeVYfnoLa7g25SseUDc92OZrMKKHySyJrFg==", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + } + } + }, + "dependencies": { + "@angular/common": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-14.0.5.tgz", + "integrity": "sha512-YFRPxx3yRLjk0gPL7tm/97mi8+Pjt3q6zWCjrLkAlDjniDvgmKNWIQ1h6crZQR0Cw7yNqK0QoFXQgTw0GJIWLQ==", + "peer": true, + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/core": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-14.0.5.tgz", + "integrity": "sha512-4MIfFM2nD+N0/Dk8xKfKvbdS/zYRhQgdnKT6ZIIV7Y/XCfn5QAIa4+vB5BEAZpuzSsZHLVdBQQ0TkaiONLfL2Q==", + "peer": true, + "requires": { + "tslib": "^2.3.0" + } + }, + "@types/jasmine": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.0.3.tgz", + "integrity": "sha512-Opp1LvvEuZdk8fSSvchK2mZwhVrsNT0JgJE9Di6MjnaIpmEXM8TLCPPrVtNTYh8+5MPdY8j9bAHMu2SSfwpZJg==", + "dev": true + }, + "rxjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz", + "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==", + "peer": true, + "requires": { + "tslib": "^2.1.0" + } + }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "zone.js": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.6.tgz", + "integrity": "sha512-umJqFtKyZlPli669gB1gOrRE9hxUUGkZr7mo878z+NEBJZZixJkKeVYfnoLa7g25SseUDc92OZrMKKHySyJrFg==", + "peer": true, + "requires": { + "tslib": "^2.3.0" + } + } + } +} diff --git a/desktop/angular/projects/safing/portmaster-api/package.json b/desktop/angular/projects/safing/portmaster-api/package.json new file mode 100644 index 00000000..98483319 --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/package.json @@ -0,0 +1,14 @@ +{ + "name": "@safing/portmaster-api", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "^14.0.0", + "@angular/core": "^14.0.0" + }, + "dependencies": { + "tslib": "^2.3.0" + }, + "devDependencies": { + "@types/jasmine": "^4.0.3" + } +} diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/app-profile.service.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/app-profile.service.ts new file mode 100644 index 00000000..814b67ff --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/app-profile.service.ts @@ -0,0 +1,262 @@ +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { Inject, Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { filter, finalize, map, mergeMap, share, take } from 'rxjs/operators'; +import { + AppProfile, + FlatConfigObject, + LayeredProfile, + TagDescription, + flattenProfileConfig, +} from './app-profile.types'; +import { + PORTMASTER_HTTP_API_ENDPOINT, + PortapiService, +} from './portapi.service'; +import { Process } from './portapi.types'; + +@Injectable({ + providedIn: 'root', +}) +export class AppProfileService { + private watchedProfiles = new Map>(); + + constructor( + private portapi: PortapiService, + private http: HttpClient, + @Inject(PORTMASTER_HTTP_API_ENDPOINT) private httpAPI: string + ) { } + + /** + * Returns the database key of a profile. + * + * @param source The source of the profile. + * @param id The profile ID. + */ + getKey(source: string, id: string): string; + + /** + * Returns the database key of a profile + * + * @param p The app-profile itself.. + */ + getKey(p: AppProfile): string; + + getKey(idOrSourceOrProfile: string | AppProfile, id?: string): string { + if (typeof idOrSourceOrProfile === 'object') { + return this.getKey(idOrSourceOrProfile.Source, idOrSourceOrProfile.ID); + } + + let key = idOrSourceOrProfile; + + if (!!id) { + key = `core:profiles/${idOrSourceOrProfile}/${id}`; + } + + return key; + } + + /** + * Load an application profile. + * + * @param sourceAndId The full profile ID including source + */ + getAppProfile(sourceAndId: string): Observable; + + /** + * Load an application profile. + * + * @param source The source of the profile + * @param id The ID of the profile + */ + getAppProfile(source: string, id: string): Observable; + + getAppProfile( + sourceOrSourceAndID: string, + id?: string + ): Observable { + let source = sourceOrSourceAndID; + if (id !== undefined) { + source += '/' + id; + } + const key = `core:profiles/${source}`; + + if (this.watchedProfiles.has(key)) { + return this.watchedProfiles.get(key)!.pipe(take(1)); + } + + return this.getAppProfileFromKey(key); + } + + setProfileIcon( + content: string | ArrayBuffer, + mimeType: string + ): Observable<{ filename: string }> { + return this.http.post<{ filename: string }>( + `${this.httpAPI}/v1/profile/icon`, + content, + { + headers: new HttpHeaders({ + 'Content-Type': mimeType, + }), + } + ); + } + + /** + * Loads an application profile by it's database key. + * + * @param key The key of the application profile. + */ + getAppProfileFromKey(key: string): Observable { + return this.portapi.get(key); + } + + /** + * Loads the global-configuration profile. + */ + globalConfig(): Observable { + return this.getAppProfile('special', 'global-config').pipe( + map((profile) => flattenProfileConfig(profile.Config)) + ); + } + + /** Returns all possible process tags. */ + tagDescriptions(): Observable { + return this.http + .get<{ Tags: TagDescription[] }>(`${this.httpAPI}/v1/process/tags`) + .pipe(map((result) => result.Tags)); + } + + /** + * Watches an application profile for changes. + * + * @param source The source of the profile + * @param id The ID of the profile + */ + watchAppProfile(sourceAndId: string): Observable; + /** + * Watches an application profile for changes. + * + * @param source The source of the profile + * @param id The ID of the profile + */ + watchAppProfile(source: string, id: string): Observable; + + watchAppProfile(sourceAndId: string, id?: string): Observable { + let key = ''; + + if (id === undefined) { + key = sourceAndId; + if (!key.startsWith('core:profiles/')) { + key = `core:profiles/${key}`; + } + } else { + key = `core:profiles/${sourceAndId}/${id}`; + } + + if (this.watchedProfiles.has(key)) { + return this.watchedProfiles.get(key)!; + } + + const stream = this.portapi.get(key).pipe( + mergeMap(() => this.portapi.watch(key)), + finalize(() => { + console.log( + 'watchAppProfile: removing cached profile stream for ' + key + ); + this.watchedProfiles.delete(key); + }), + share({ + connector: () => new BehaviorSubject(null), + resetOnRefCountZero: true, + }), + filter((profile) => profile !== null) + ) as Observable; + + this.watchedProfiles.set(key, stream); + + return stream; + } + + /** @deprecated use saveProfile instead */ + saveLocalProfile(profile: AppProfile): Observable { + return this.saveProfile(profile); + } + + /** + * Save an application profile. + * + * @param profile The profile to save + */ + saveProfile(profile: AppProfile): Observable { + profile.LastEdited = Math.floor(new Date().getTime() / 1000); + return this.portapi.update( + `core:profiles/${profile.Source}/${profile.ID}`, + profile + ); + } + + /** + * Watch all application profiles + */ + watchProfiles(): Observable { + return this.portapi.watchAll('core:profiles/'); + } + + watchLayeredProfile(source: string, id: string): Observable; + + /** + * Watches the layered runtime profile for a given application + * profile. + * + * @param profile The app profile + */ + watchLayeredProfile(profile: AppProfile): Observable; + + watchLayeredProfile( + profileOrSource: string | AppProfile, + id?: string + ): Observable { + if (typeof profileOrSource == 'object') { + id = profileOrSource.ID; + profileOrSource = profileOrSource.Source; + } + + const key = `runtime:layeredProfile/${profileOrSource}/${id}`; + return this.portapi.watch(key); + } + + /** + * Loads the layered runtime profile for a given application + * profile. + * + * @param profile The app profile + */ + getLayeredProfile(profile: AppProfile): Observable { + const key = `runtime:layeredProfile/${profile.Source}/${profile.ID}`; + return this.portapi.get(key); + } + + /** + * Delete an application profile. + * + * @param profile The profile to delete + */ + deleteProfile(profile: AppProfile): Observable { + return this.portapi.delete(`core:profiles/${profile.Source}/${profile.ID}`); + } + + getProcessesByProfile(profileOrId: AppProfile | string): Observable { + if (typeof profileOrId === 'object') { + profileOrId = profileOrId.Source + "/" + profileOrId.ID + } + + return this.http.get(`${this.httpAPI}/v1/process/list/by-profile/${profileOrId}`) + } + + getProcessByPid(pid: number): Observable { + return this.http.get(`${this.httpAPI}/v1/process/group-leader/${pid}`) + } +} + diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/app-profile.types.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/app-profile.types.ts new file mode 100644 index 00000000..986d62ff --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/app-profile.types.ts @@ -0,0 +1,215 @@ +import { BaseSetting, OptionValueType, SettingValueType } from './config.types'; +import { SecurityLevel } from './core.types'; +import { Record } from './portapi.types'; + +export interface ConfigMap { + [key: string]: ConfigObject; +} + +export type ConfigObject = OptionValueType | ConfigMap; + +export interface FlatConfigObject { + [key: string]: OptionValueType; +} + + +export interface LayeredProfile extends Record { + // LayerIDs is a list of all profiles that are used + // by this layered profile. Profiles are evaluated in + // order. + LayerIDs: string[]; + + // The current revision counter of the layered profile. + RevisionCounter: number; +} + +export enum FingerprintType { + Tag = 'tag', + Cmdline = 'cmdline', + Env = 'env', + Path = 'path', +} + +export enum FingerpringOperation { + Equal = 'equals', + Prefix = 'prefix', + Regex = 'regex', +} + +export interface Fingerprint { + Type: FingerprintType; + Key: string; + Operation: FingerpringOperation; + Value: string; +} + +export interface TagDescription { + ID: string; + Name: string; + Description: string; +} + +export interface Icon { + Type: 'database' | 'path' | 'api'; + Source: '' | 'user' | 'import' | 'core' | 'ui'; + Value: string; +} + +export interface AppProfile extends Record { + ID: string; + LinkedPath: string; // deprecated + PresentationPath: string; + Fingerprints: Fingerprint[]; + Created: number; + LastEdited: number; + Config?: ConfigMap; + Description: string; + Warning: string; + WarningLastUpdated: string; + Homepage: string; + Icons: Icon[]; + Name: string; + Internal: boolean; + SecurityLevel: SecurityLevel; + Source: 'local'; +} + +// flattenProfileConfig returns a flat version of a nested ConfigMap where each property +// can be used as the database key for the associated setting. +export function flattenProfileConfig( + p?: ConfigMap, + prefix = '' +): FlatConfigObject { + if (p === null || p === undefined) { + return {} + } + + let result: FlatConfigObject = {}; + + Object.keys(p).forEach((key) => { + const childPrefix = prefix === '' ? key : `${prefix}/${key}`; + + const prop = p[key]; + + if (isConfigMap(prop)) { + const flattened = flattenProfileConfig(prop, childPrefix); + result = mergeObjects(result, flattened); + return; + } + + result[childPrefix] = prop; + }); + + return result; +} + +/** + * Returns the current value (or null) of a setting stored in a config + * map by path. + * + * @param obj The ConfigMap object + * @param path The path of the setting separated by foward slashes. + */ +export function getAppSetting( + obj: ConfigMap | null | undefined, + path: string +): T | null { + if (obj === null || obj === undefined) { + return null + } + + const parts = path.split('/'); + + let iter = obj; + for (let idx = 0; idx < parts.length; idx++) { + const propName = parts[idx]; + + if (iter[propName] === undefined) { + return null; + } + + const value = iter[propName]; + if (idx === parts.length - 1) { + return value as T; + } + + if (!isConfigMap(value)) { + return null; + } + + iter = value; + } + return null; +} + +export function getActualValue>( + s: S +): SettingValueType { + if (s.Value !== undefined) { + return s.Value; + } + if (s.GlobalDefault !== undefined) { + return s.GlobalDefault; + } + return s.DefaultValue; +} + +/** + * Sets the value of a settings inside the nested config object. + * + * @param obj THe config object + * @param path The path of the setting + * @param value The new value to set. + */ +export function setAppSetting(obj: ConfigObject, path: string, value: any) { + const parts = path.split('/'); + if (typeof obj !== 'object' || Array.isArray(obj)) { + return; + } + + let iter = obj; + for (let idx = 0; idx < parts.length; idx++) { + const propName = parts[idx]; + + if (idx === parts.length - 1) { + if (value === undefined) { + delete iter[propName]; + } else { + iter[propName] = value; + } + return; + } + + if (iter[propName] === undefined) { + iter[propName] = {}; + } + + iter = iter[propName] as ConfigMap; + } +} + +/** Typeguard to ensure v is a ConfigMap */ +function isConfigMap(v: any): v is ConfigMap { + return typeof v === 'object' && !Array.isArray(v); +} + +/** + * Returns a new flat-config object that contains values from both + * parameters. + * + * @param a The first config object + * @param b The second config object + */ +function mergeObjects( + a: FlatConfigObject, + b: FlatConfigObject +): FlatConfigObject { + var res: FlatConfigObject = {}; + Object.keys(a).forEach((key) => { + res[key] = a[key]; + }); + Object.keys(b).forEach((key) => { + res[key] = b[key]; + }); + return res; +} diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/config.service.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/config.service.ts new file mode 100644 index 00000000..58daeb28 --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/config.service.ts @@ -0,0 +1,128 @@ +import { Injectable, TrackByFunction } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { distinctUntilChanged, filter, map, share, toArray } from 'rxjs/operators'; +import { BaseSetting, BoolSetting, OptionType, Setting, SettingValueType } from './config.types'; +import { PortapiService } from './portapi.service'; + + +@Injectable({ + providedIn: 'root' +}) +export class ConfigService { + networkRatingEnabled$: Observable; + + /** + * A {@link TrackByFunction} for tracking settings. + */ + static trackBy: TrackByFunction = (_: number, obj: Setting) => obj.Name; + readonly trackBy = ConfigService.trackBy; + + /** configPrefix is the database key prefix for the config db */ + readonly configPrefix = "config:"; + + constructor(private portapi: PortapiService) { + this.networkRatingEnabled$ = this.watch("core/enableNetworkRating") + .pipe( + share({ connector: () => new BehaviorSubject(false) }), + ) + } + + /** + * Loads a configuration setting from the database. + * + * @param key The key of the configuration setting. + */ + get(key: string): Observable { + return this.portapi.get(this.configPrefix + key); + } + + /** + * Returns all configuration settings that match query. Note that in + * contrast to {@link PortAPI} settings values are collected into + * an array before being emitted. This allows simple usage in *ngFor + * and friends. + * + * @param query The query used to search for configuration settings. + */ + query(query: string): Observable { + return this.portapi.query(this.configPrefix + query) + .pipe( + map(setting => setting.data), + toArray() + ); + } + + /** + * Save a setting. + * + * @param s The setting to save. Note that the new value should already be set to {@property Value}. + */ + save(s: Setting): Observable; + + /** + * Save a setting. + * + * @param key The key of the configuration setting + * @param value The new value of the setting. + */ + save(key: string, value: any): Observable; + + // save is overloaded, see above. + save(s: Setting | string, v?: any): Observable { + if (typeof s === 'string') { + return this.portapi.update(this.configPrefix + s, { + Key: s, + Value: v, + }); + } + return this.portapi.update(this.configPrefix + s.Key, s); + } + + /** + * Watch a configuration setting. + * + * @param key The key of the setting to watch. + */ + watch(key: string): Observable> { + return this.portapi.qsub, any>>(this.configPrefix + key) + .pipe( + filter(value => value.key === this.configPrefix + key), // qsub does a query so filter for our key. + map(value => value.data), + map(value => value.Value !== undefined ? value.Value : value.DefaultValue), + distinctUntilChanged(), + ) + } + + /** + * Tests if a value is valid for a given option. + * + * @param spec The option specification (as returned by get()). + * @param value The value that should be tested. + */ + validate(spec: S, value: SettingValueType) { + if (!spec.ValidationRegex) { + return; + } + + const re = new RegExp(spec.ValidationRegex); + + switch (spec.OptType) { + case OptionType.Int: + case OptionType.Bool: + // todo(ppacher): do we validate that? + return + case OptionType.String: + if (!re.test(value as string)) { + throw new Error(`${value} does not match ${spec.ValidationRegex}`) + } + return; + case OptionType.StringArray: + (value as string[]).forEach(v => { + if (!re.test(v as string)) { + throw new Error(`${value} does not match ${spec.ValidationRegex}`) + } + }); + return + } + } +} diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/config.types.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/config.types.ts new file mode 100644 index 00000000..99fe5d82 --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/config.types.ts @@ -0,0 +1,348 @@ +import { FeatureID } from './features'; +import { Record } from './portapi.types'; +import { deepClone } from './utils'; + +/** + * ExpertiseLevel defines all available expertise levels. + */ +export enum ExpertiseLevel { + User = 'user', + Expert = 'expert', + Developer = 'developer', +} + +export enum ExpertiseLevelNumber { + user = 0, + expert = 1, + developer = 2 +} + +export function getExpertiseLevelNumber(lvl: ExpertiseLevel): ExpertiseLevelNumber { + switch (lvl) { + case ExpertiseLevel.User: + return ExpertiseLevelNumber.user; + case ExpertiseLevel.Expert: + return ExpertiseLevelNumber.expert; + case ExpertiseLevel.Developer: + return ExpertiseLevelNumber.developer + } +} + +/** + * OptionType defines the type of an option as stored in + * the backend. Note that ExternalOptionHint may be used + * to request a different visual representation and edit + * menu on a per-option basis. + */ +export enum OptionType { + String = 1, + StringArray = 2, + Int = 3, + Bool = 4, +} + +/** + * Converts an option type to it's string representation. + * + * @param opt The option type to convert + */ +export function optionTypeName(opt: OptionType): string { + switch (opt) { + case OptionType.String: + return 'string'; + case OptionType.StringArray: + return '[]string'; + case OptionType.Int: + return 'int' + case OptionType.Bool: + return 'bool' + } +} + +/** The actual type an option value can be */ +export type OptionValueType = string | string[] | number | boolean; + +/** Type-guard for string option types */ +export function isStringType(opt: OptionType, vt: OptionValueType): vt is string { + return opt === OptionType.String; +} + +/** Type-guard for string-array option types */ +export function isStringArrayType(opt: OptionType, vt: OptionValueType): vt is string[] { + return opt === OptionType.StringArray; +} + +/** Type-guard for number option types */ +export function isNumberType(opt: OptionType, vt: OptionValueType): vt is number { + return opt === OptionType.Int; +} + +/** Type-guard for boolean option types */ +export function isBooleanType(opt: OptionType, vt: OptionValueType): vt is boolean { + return opt === OptionType.Bool; +} + +/** + * ReleaseLevel defines the available release and maturity + * levels. + */ +export enum ReleaseLevel { + Stable = 0, + Beta = 1, + Experimental = 2, +} + +export function releaseLevelFromName(name: 'stable' | 'beta' | 'experimental'): ReleaseLevel { + switch (name) { + case 'stable': + return ReleaseLevel.Stable; + case 'beta': + return ReleaseLevel.Beta; + case 'experimental': + return ReleaseLevel.Experimental; + } +} + +/** + * releaseLevelName returns a string representation of the + * release level. + * + * @args level The release level to convert. + */ +export function releaseLevelName(level: ReleaseLevel): string { + switch (level) { + case ReleaseLevel.Stable: + return 'stable' + case ReleaseLevel.Beta: + return 'beta' + case ReleaseLevel.Experimental: + return 'experimental' + } +} + +/** + * ExternalOptionHint tells the UI to use a different visual + * representation and edit menu that the options value would + * imply. + */ +export enum ExternalOptionHint { + SecurityLevel = 'security level', + EndpointList = 'endpoint list', + FilterList = 'filter list', + OneOf = 'one-of', + OrderedList = 'ordered' +} + +/** A list of well-known option annotation keys. */ +export enum WellKnown { + DisplayHint = "safing/portbase:ui:display-hint", + Order = "safing/portbase:ui:order", + Unit = "safing/portbase:ui:unit", + Category = "safing/portbase:ui:category", + Subsystem = "safing/portbase:module:subsystem", + Stackable = "safing/portbase:options:stackable", + QuickSetting = "safing/portbase:ui:quick-setting", + Requires = "safing/portbase:config:requires", + RestartPending = "safing/portbase:options:restart-pending", + EndpointListVerdictNames = "safing/portmaster:ui:endpoint-list:verdict-names", + RequiresFeatureID = "safing/portmaster:ui:config:requires-feature", + RequiresUIReload = "safing/portmaster:ui:requires-reload", +} + +/** + * Annotations describes the annoations object of a configuration + * setting. Well-known annotations are stricktly typed. + */ +export interface Annotations { + // Well known option annoations and their + // types. + [WellKnown.DisplayHint]?: ExternalOptionHint; + [WellKnown.Order]?: number; + [WellKnown.Unit]?: string; + [WellKnown.Category]?: string; + [WellKnown.Subsystem]?: string; + [WellKnown.Stackable]?: true; + [WellKnown.QuickSetting]?: QuickSetting | QuickSetting[] | CountrySelectionQuickSetting | CountrySelectionQuickSetting[]; + [WellKnown.Requires]?: ValueRequirement | ValueRequirement[]; + [WellKnown.RequiresFeatureID]?: FeatureID | FeatureID[]; + [WellKnown.RequiresUIReload]?: unknown, + // Any thing else... + [key: string]: any; +} + +export interface PossilbeValue { + /** Name is the name of the value and should be displayed */ + Name: string; + /** Description may hold an additional description of the value */ + Description: string; + /** Value is the actual value expected by the portmaster */ + Value: T; +} + +export interface QuickSetting { + // Name is the name of the quick setting. + Name: string; + // Value is the value that the quick-setting configures. It must match + // the expected value type of the annotated option. + Value: T; + // Action defines the action of the quick setting. + Action: 'replace' | 'merge-top' | 'merge-bottom'; +} + +export interface CountrySelectionQuickSetting extends QuickSetting { + // Filename of the flag to be used. + // In most cases this will be the 2-letter country code, but there are also special flags. + FlagID: string; +} + +export interface ValueRequirement { + // Key is the configuration key of the required setting. + Key: string; + // Value is the required value of the linked setting. + Value: any; +} + +/** + * BaseSetting describes the general shape of a portbase config setting. + */ +export interface BaseSetting extends Record { + // Value is the value of a setting. + Value?: T; + // DefaultValue is the default value of a setting. + DefaultValue: T; + // Description is a short description. + Description?: string; + // ExpertiseLevel defines the required expertise level for + // this setting to show up. + ExpertiseLevel: ExpertiseLevelNumber; + // Help may contain a longer help text for this option. + Help?: string; + // Key is the database key. + Key: string; + // Name is the name of the option. + Name: string; + // OptType is the option's basic type. + OptType: O; + // Annotations holds option specific annotations. + Annotations: Annotations; + // ReleaseLevel defines the release level of the feature + // or settings changed by this option. + ReleaseLevel: ReleaseLevel; + // RequiresRestart may be set to true if the service requires + // a restart after this option has been changed. + RequiresRestart?: boolean; + // ValidateRegex defines the regex used to validate this option. + // The regex is used in Golang but is expected to be valid in + // JavaScript as well. + ValidationRegex?: string; + PossibleValues?: PossilbeValue[]; + + // GlobalDefault holds the global default value and is used in the app settings + // This property is NOT defined inside the portmaster! + GlobalDefault?: T; +} + +export type IntSetting = BaseSetting; +export type StringSetting = BaseSetting; +export type StringArraySetting = BaseSetting; +export type BoolSetting = BaseSetting; + +/** + * Apply a quick setting to a value. + * + * @param current The current value of the setting. + * @param qs The quick setting to apply. + */ +export function applyQuickSetting(current: V | null, qs: QuickSetting): V | null { + if (qs.Action === 'replace' || !qs.Action) { + return deepClone(qs.Value); + } + + if ((!Array.isArray(current) && current !== null) || !Array.isArray(qs.Value)) { + console.warn(`Tried to ${qs.Action} quick-setting on non-array type`); + return current; + } + + const clone = deepClone(current); + let missing: any[] = []; + + qs.Value.forEach(val => { + if (clone.includes(val)) { + return + } + missing.push(val); + }); + + if (qs.Action === 'merge-bottom') { + return clone.concat(missing) as V; + } + + return missing.concat(clone) as V; +} + +/** + * Parses the ValidationRegex of a setting and returns a list + * of supported values. + * + * @param s The setting to extract support values from. + */ +export function parseSupportedValues(s: S): SettingValueType[] { + if (!s.ValidationRegex) { + return []; + } + + const values = s.ValidationRegex.match(/\w+/gmi); + const result: SettingValueType[] = []; + + let converter: (s: string) => any; + + switch (s.OptType) { + case OptionType.Bool: + converter = s => s === 'true'; + break; + case OptionType.Int: + converter = s => +s; + break; + case OptionType.String: + case OptionType.StringArray: + converter = s => s + break + } + + values?.forEach(val => { + result.push(converter(val)) + }); + + return result; +} + +/** + * isDefaultValue checks if value is the settings default value. + * It supports all available settings type and fallsback to use + * JSON encoded string comparision (JS JSON.stringify is stable). + */ +export function isDefaultValue(value: T | undefined | null, defaultValue: T): boolean { + if (value === undefined) { + return true; + } + + const isObject = typeof value === 'object'; + const isDefault = isObject + ? JSON.stringify(value) === JSON.stringify(defaultValue) + : value === defaultValue; + + return isDefault; +} + +/** + * SettingValueType is used to infer the type of a settings from it's default value. + * Use like this: + * + * validate(spec: S, value SettingValueType) { ... } + */ +export type SettingValueType = S extends { DefaultValue: infer T } ? T : any; + +export type Setting = IntSetting + | StringSetting + | StringArraySetting + | BoolSetting; diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/core.types.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/core.types.ts new file mode 100644 index 00000000..5e5e1417 --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/core.types.ts @@ -0,0 +1,34 @@ +import { TrackByFunction } from '@angular/core'; + +export enum SecurityLevel { + Off = 0, + Normal = 1, + High = 2, + Extreme = 4, +} + +export enum RiskLevel { + Off = 'off', + Auto = 'auto', + Low = 'low', + Medium = 'medium', + High = 'high' +} + +/** Interface capturing any object that has an ID member. */ +export interface Identifyable { + ID: string | number; +} + +/** A TrackByFunction for all Identifyable objects. */ +export const trackById: TrackByFunction = (_: number, obj: Identifyable) => { + return obj.ID; +} + +export function getEnumKey(enumLike: any, value: string | number): string { + if (typeof value === 'string') { + return value.toLowerCase() + } + + return (enumLike[value] as string).toLowerCase() +} diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/debug-api.service.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/debug-api.service.ts new file mode 100644 index 00000000..f0617943 --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/debug-api.service.ts @@ -0,0 +1,54 @@ +import { HttpClient } from '@angular/common/http'; +import { Inject, Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { PORTMASTER_HTTP_API_ENDPOINT } from './portapi.service'; + +@Injectable({ + providedIn: 'root', +}) +export class DebugAPI { + constructor( + private http: HttpClient, + @Inject(PORTMASTER_HTTP_API_ENDPOINT) private httpAPI: string, + ) { } + + ping(): Observable { + return this.http.get(`${this.httpAPI}/v1/ping`, { + responseType: 'text' + }) + } + + getStack(): Observable { + return this.http.get(`${this.httpAPI}/v1/debug/stack`, { + responseType: 'text' + }) + } + + getDebugInfo(style = 'github'): Observable { + return this.http.get(`${this.httpAPI}/v1/debug/info`, { + params: { + style, + }, + responseType: 'text', + }) + } + + getCoreDebugInfo(style = 'github'): Observable { + return this.http.get(`${this.httpAPI}/v1/debug/core`, { + params: { + style, + }, + responseType: 'text', + }) + } + + getProfileDebugInfo(source: string, id: string, style = 'github'): Observable { + return this.http.get(`${this.httpAPI}/v1/debug/network`, { + params: { + profile: `${source}/${id}`, + style, + }, + responseType: 'text', + }) + } +} diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/features.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/features.ts new file mode 100644 index 00000000..658f1c1b --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/features.ts @@ -0,0 +1,8 @@ +export enum FeatureID { + None = "", + SPN = "spn", + PrioritySupport = "support", + History = "history", + Bandwidth = "bw-vis", + VPNCompat = "vpn-compat", +} diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/meta-api.service.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/meta-api.service.ts new file mode 100644 index 00000000..009848f4 --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/meta-api.service.ts @@ -0,0 +1,106 @@ +import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http'; +import { Inject, Injectable, Optional } from '@angular/core'; +import { Observable, of, throwError } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { PORTMASTER_HTTP_API_ENDPOINT } from './portapi.service'; + +export interface MetaEndpointParameter { + Method: string; + Field: string; + Value: string; + Description: string; +} + +export interface MetaEndpoint { + Path: string; + MimeType: string; + Read: number; + Write: number; + Name: string; + Description: string; + Parameters: MetaEndpointParameter[]; +} + +export interface AuthPermission { + Read: number; + Write: number; + ReadRole: string; + WriteRole: string; +} + +export interface MyProfileResponse { + profile: string; + source: string; + name: string; +} + +export interface AuthKeyResponse { + key: string; + validUntil: string; +} + +@Injectable() +export class MetaAPI { + constructor( + private http: HttpClient, + @Inject(PORTMASTER_HTTP_API_ENDPOINT) @Optional() private httpEndpoint: string = 'http://localhost:817/api', + ) { } + + listEndpoints(): Observable { + return this.http.get(`${this.httpEndpoint}/v1/endpoints`) + } + + permissions(): Observable { + return this.http.get(`${this.httpEndpoint}/v1/auth/permissions`) + } + + myProfile(): Observable { + return this.http.get(`${this.httpEndpoint}/v1/app/profile`) + } + + requestApplicationAccess(appName: string, read: 'user' | 'admin' = 'user', write: 'user' | 'admin' = 'user'): Observable { + let params = new HttpParams() + .set("app-name", appName) + .set("read", read) + .set("write", write) + + return this.http.get(`${this.httpEndpoint}/v1/app/auth`, { params }) + } + + login(bearer: string): Observable; + login(username: string, password: string): Observable; + login(usernameOrBearer: string, password?: string): Observable { + let login: Observable; + + if (!!password) { + login = this.http.get(`${this.httpEndpoint}/v1/auth/basic`, { + headers: { + 'Authorization': `Basic ${btoa(usernameOrBearer + ":" + password)}` + } + }) + } else { + login = this.http.get(`${this.httpEndpoint}/v1/auth/bearer`, { + headers: { + 'Authorization': `Bearer ${usernameOrBearer}` + } + }) + } + + return login.pipe( + map(() => true), + catchError(err => { + if (err instanceof HttpErrorResponse) { + if (err.status === 401) { + return of(false); + } + } + + return throwError(() => err) + }) + ) + } + + logout(): Observable { + return this.http.get(`${this.httpEndpoint}/v1/auth/reset`); + } +} diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/module.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/module.ts new file mode 100644 index 00000000..0ed13363 --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/module.ts @@ -0,0 +1,55 @@ +import { ModuleWithProviders, NgModule } from "@angular/core"; +import { AppProfileService } from "./app-profile.service"; +import { ConfigService } from "./config.service"; +import { DebugAPI } from "./debug-api.service"; +import { MetaAPI } from "./meta-api.service"; +import { Netquery } from "./netquery.service"; +import { PortapiService, PORTMASTER_HTTP_API_ENDPOINT, PORTMASTER_WS_API_ENDPOINT } from "./portapi.service"; +import { SPNService } from "./spn.service"; +import { WebsocketService } from "./websocket.service"; + +export interface ModuleConfig { + httpAPI?: string; + websocketAPI?: string; +} + +@NgModule({}) +export class PortmasterAPIModule { + + /** + * Configures a module with additional providers. + * + * @param cfg The module configuration defining the Portmaster HTTP and Websocket API endpoints. + */ + static forRoot(cfg: ModuleConfig = {}): ModuleWithProviders { + if (cfg.httpAPI === undefined) { + cfg.httpAPI = `http://${window.location.host}/api`; + } + if (cfg.websocketAPI === undefined) { + cfg.websocketAPI = `ws://${window.location.host}/api/database/v1`; + } + + return { + ngModule: PortmasterAPIModule, + providers: [ + PortapiService, + WebsocketService, + MetaAPI, + ConfigService, + AppProfileService, + DebugAPI, + Netquery, + SPNService, + { + provide: PORTMASTER_HTTP_API_ENDPOINT, + useValue: cfg.httpAPI, + }, + { + provide: PORTMASTER_WS_API_ENDPOINT, + useValue: cfg.websocketAPI + } + ] + } + } + +} diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/netquery.service.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/netquery.service.ts new file mode 100644 index 00000000..c0b1ec88 --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/netquery.service.ts @@ -0,0 +1,543 @@ +import { HttpClient, HttpParams, HttpResponse } from "@angular/common/http"; +import { Inject, Injectable } from "@angular/core"; +import { Observable, forkJoin, of } from "rxjs"; +import { catchError, map, mergeMap } from "rxjs/operators"; +import { AppProfileService } from "./app-profile.service"; +import { AppProfile } from "./app-profile.types"; +import { DNSContext, IPScope, Reason, TLSContext, TunnelContext, Verdict } from "./network.types"; +import { PORTMASTER_HTTP_API_ENDPOINT, PortapiService } from "./portapi.service"; +import { Container } from "postcss"; + +export interface FieldSelect { + field: string; +} + +export interface FieldAsSelect { + $field: { + field: string; + as: string; + } +} + +export interface Count { + $count: { + field: string; + distinct?: boolean; + as?: string; + } +} + +export interface Sum { + $sum: { + condition: Condition; + as: string; + distinct?: boolean; + } | { + field: string; + as: string; + distinct?: boolean; + } +} + +export interface Min { + $min: { + condition: Condition; + as: string; + distinct?: boolean; + } | { + field: string; + as: string; + distinct?: boolean; + } +} + +export interface Distinct { + $distinct: string; +} + +export type Select = FieldSelect | FieldAsSelect | Count | Distinct | Sum | Min; + +export interface Equal { + $eq: any; +} + +export interface NotEqual { + $ne: any; +} + +export interface Like { + $like: string; +} + +export interface In { + $in: any[]; +} + +export interface NotIn { + $notin: string[]; +} + +export interface Greater { + $gt: number; +} + +export interface GreaterOrEqual { + $ge: number; +} + +export interface Less { + $lt: number; +} + +export interface LessOrEqual { + $le: number; +} + +export type Matcher = Equal | NotEqual | Like | In | NotIn | Greater | GreaterOrEqual | Less | LessOrEqual; + +export interface OrderBy { + field: string; + desc?: boolean; +} + +export interface Condition { + [key: string]: string | Matcher | (string | Matcher)[]; +} + +export interface TextSearch { + fields: string[]; + value: string; +} + +export enum Database { + Live = "main", + History = "history" +} + +export interface Query { + select?: string | Select | (Select | string)[]; + query?: Condition; + orderBy?: string | OrderBy | (OrderBy | string)[]; + textSearch?: TextSearch; + groupBy?: string[]; + pageSize?: number; + page?: number; + databases?: Database[]; +} + +export interface NetqueryConnection { + id: string; + allowed: boolean | null; + profile: string; + path: string; + type: 'dns' | 'ip'; + external: boolean; + ip_version: number; + ip_protocol: number; + local_ip: string; + local_port: number; + remote_ip: string; + remote_port: number; + domain: string; + country: string; + asn: number; + as_owner: string; + latitude: number; + longitude: number; + scope: IPScope; + verdict: Verdict; + started: string; + ended: string; + tunneled: boolean; + encrypted: boolean; + internal: boolean; + direction: 'inbound' | 'outbound'; + profile_revision: number; + exit_node?: string; + extra_data?: { + pid?: number; + processCreatedAt?: number; + cname?: string[]; + blockedByLists?: string[]; + blockedEntities?: string[]; + reason?: Reason; + tunnel?: TunnelContext; + dns?: DNSContext; + tls?: TLSContext; + }; + + profile_name: string; + active: boolean; + bytes_received: number; + bytes_sent: number; +} + +export interface ChartResult { + timestamp: number; + value: number; + countBlocked: number; +} + +export interface QueryResult extends Partial { + [key: string]: any; +} + +export interface Identities { + exit_node: string; + count: number; +} + +export interface IProfileStats { + ID: string; + Name: string; + + size: number; + empty: boolean; + identities: Identities[]; + countAllowed: number; + countUnpermitted: number; + countAliveConnections: number; + bytes_sent: number; + bytes_received: number; +} + +type BatchResponse = { + [key in keyof T]: QueryResult[] +} + +interface BatchRequest { + [key: string]: Query +} + +interface BandwidthBaseResult { + timestamp: number; + incoming: number; + outgoing: number; +} + +export type ConnKeys = keyof NetqueryConnection + +export type BandwidthChartResult = { + [key in K]: NetqueryConnection[K]; +} & BandwidthBaseResult + +export type ProfileBandwidthChartResult = BandwidthChartResult<'profile'>; + +export type ConnectionBandwidthChartResult = BandwidthChartResult<'id'>; + +@Injectable({ providedIn: 'root' }) +export class Netquery { + constructor( + private http: HttpClient, + private profileService: AppProfileService, + private portapi: PortapiService, + @Inject(PORTMASTER_HTTP_API_ENDPOINT) private httpAPI: string, + ) { } + + query(query: Query, origin: string): Observable { + return this.http.post<{ results: QueryResult[] }>(`${this.httpAPI}/v1/netquery/query`, query, { + params: new HttpParams().set("origin", origin) + }) + .pipe(map(res => res.results || [])); + } + + batch(queries: T): Observable> { + return this.http.post>(`${this.httpAPI}/v1/netquery/query/batch`, queries) + } + + cleanProfileHistory(profileIDs: string | string[]): Observable> { + return this.http.post(`${this.httpAPI}/v1/netquery/history/clear`, + { + profileIDs: Array.isArray(profileIDs) ? profileIDs : [profileIDs] + }, + { + observe: 'response', + responseType: 'text', + reportProgress: false, + } + ) + } + + profileBandwidthChart(profile?: string[], interval?: number): Observable<{ [profile: string]: ProfileBandwidthChartResult[] }> { + const cond: Condition = {} + if (!!profile) { + cond['profile'] = profile + } + + return this.bandwidthChart(cond, ['profile'], interval) + .pipe( + map(results => { + const obj: { + [connId: string]: ProfileBandwidthChartResult[] + } = {}; + + results?.forEach(row => { + const arr = obj[row.profile] || [] + arr.push(row) + obj[row.profile] = arr + }) + + return obj + }) + ) + } + + bandwidthChart(query: Condition, groupBy?: K[], interval?: number): Observable[]> { + return this.http.post<{ results: BandwidthChartResult[] }>(`${this.httpAPI}/v1/netquery/charts/bandwidth`, { + interval, + groupBy, + query, + }) + .pipe( + map(response => response.results), + ) + } + + connectionBandwidthChart(connIds: string[], interval?: number): Observable<{ [connId: string]: ConnectionBandwidthChartResult[] }> { + const cond: Condition = {} + if (!!connIds) { + cond['id'] = connIds + } + + return this.bandwidthChart(cond, ['id'], interval) + .pipe( + map(results => { + const obj: { + [connId: string]: ConnectionBandwidthChartResult[] + } = {}; + + results?.forEach(row => { + const arr = obj[row.id] || [] + arr.push(row) + obj[row.id] = arr + }) + + return obj + }) + ) + } + + activeConnectionChart(cond: Condition, textSearch?: TextSearch): Observable { + return this.http.post<{ results: ChartResult[] }>(`${this.httpAPI}/v1/netquery/charts/connection-active`, { + query: cond, + textSearch, + }) + .pipe(map(res => { + const now = new Date(); + + let data: ChartResult[] = []; + + let lastPoint: ChartResult | null = { + timestamp: Math.floor(now.getTime() / 1000 - 600), + value: 0, + countBlocked: 0, + }; + res.results?.forEach(point => { + if (!!lastPoint && lastPoint.timestamp < (point.timestamp - 10)) { + for (let i = lastPoint.timestamp; i < point.timestamp; i += 10) { + data.push({ + timestamp: i, + value: 0, + countBlocked: 0, + }) + } + } + data.push(point); + lastPoint = point; + }) + + const lastPointTs = Math.round(now.getTime() / 1000); + if (!!lastPoint && lastPoint.timestamp < (lastPointTs - 20)) { + for (let i = lastPoint.timestamp; i < lastPointTs; i += 20) { + data.push({ + timestamp: i, + value: 0, + countBlocked: 0 + }) + } + } + + return data; + })); + } + + getActiveProfileIDs(): Observable { + return this.query({ + select: [ + 'profile', + ], + groupBy: [ + 'profile', + ], + }, 'get-active-profile-ids').pipe( + map(result => { + return result.map(res => res.profile!); + }) + ) + } + + getActiveProfiles(): Observable { + return this.getActiveProfileIDs() + .pipe( + mergeMap(profiles => forkJoin(profiles.map(pid => this.profileService.getAppProfile(pid)))) + ) + } + + getProfileStats(query?: Condition): Observable { + let profileCache = new Map(); + + return this.batch({ + verdicts: { + select: [ + 'profile', + 'verdict', + { $count: { field: '*', as: 'totalCount' } }, + ], + groupBy: [ + 'profile', + 'verdict', + ], + query: query, + }, + + conns: { + select: [ + 'profile', + { $count: { field: '*', as: 'totalCount' } }, + { $count: { field: 'ended', as: 'countEnded' } }, + { $sum: { field: 'bytes_sent', as: 'bytes_sent' } }, + { $sum: { field: 'bytes_received', as: 'bytes_received' } }, + ], + groupBy: [ + 'profile', + ], + query: query, + }, + + identities: { + select: [ + 'profile', + 'exit_node', + { $count: { field: '*', as: 'totalCount' } } + ], + groupBy: [ + 'profile', + 'exit_node', + ], + query: { + ...query, + exit_node: { + $ne: "", + }, + }, + } + }).pipe( + map(result => { + let statsMap = new Map(); + + const getOrCreate = (id: string) => { + let stats = statsMap.get(id) || { + ID: id, + Name: 'Deleted', + countAliveConnections: 0, + countAllowed: 0, + countUnpermitted: 0, + empty: true, + identities: [], + size: 0, + bytes_received: 0, + bytes_sent: 0 + }; + + statsMap.set(id, stats); + return stats; + } + result.verdicts?.forEach(res => { + const stats = getOrCreate(res.profile!); + + switch (res.verdict) { + case Verdict.Accept: + case Verdict.RerouteToNs: + case Verdict.RerouteToTunnel: + case Verdict.Undeterminable: + stats.size += res.totalCount + stats.countAllowed += res.totalCount; + break; + + case Verdict.Block: + case Verdict.Drop: + case Verdict.Failed: + case Verdict.Undecided: + stats.size += res.totalCount + stats.countUnpermitted += res.totalCount; + break; + } + + stats.empty = stats.size == 0; + }) + + result.conns?.forEach(res => { + const stats = getOrCreate(res.profile!); + + stats.countAliveConnections = res.totalCount - res.countEnded; + stats.bytes_received += res.bytes_received!; + stats.bytes_sent += res.bytes_sent!; + }) + + result.identities?.forEach(res => { + const stats = getOrCreate(res.profile!); + + let ident = stats.identities.find(value => value.exit_node === res.exit_node) + if (!ident) { + ident = { + count: 0, + exit_node: res.exit_node!, + } + stats.identities.push(ident); + } + + ident.count += res.totalCount; + }) + + return Array.from(statsMap.values()) + }), + mergeMap(stats => { + return forkJoin(stats.map(p => { + if (profileCache.has(p.ID)) { + return of(profileCache.get(p.ID)!); + } + return this.profileService.getAppProfile(p.ID) + .pipe(catchError(err => { + return of(null) + })) + })) + .pipe( + map((profiles: (AppProfile | null)[]) => { + profileCache = new Map(); + + let lm = new Map(); + stats.forEach(stat => lm.set(stat.ID, stat)); + + profiles + .forEach(p => { + if (!p) { + return + } + + profileCache.set(`${p.Source}/${p.ID}`, p) + + let stat = lm.get(`${p.Source}/${p.ID}`) + if (!stat) { + return; + } + + stat.Name = p.Name + }) + + return Array.from(lm.values()) + }) + ) + }) + ) + } +} diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/network.types.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/network.types.ts new file mode 100644 index 00000000..6cdef998 --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/network.types.ts @@ -0,0 +1,314 @@ +import { Record } from './portapi.types'; + +export enum Verdict { + Undecided = 0, + Undeterminable = 1, + Accept = 2, + Block = 3, + Drop = 4, + RerouteToNs = 5, + RerouteToTunnel = 6, + Failed = 7 +} + +export enum IPProtocol { + ICMP = 1, + IGMP = 2, + TCP = 6, + UDP = 17, + ICMPv6 = 58, + UDPLite = 136, + RAW = 255, // TODO(ppacher): what is RAW used for? +} + +export enum IPVersion { + V4 = 4, + V6 = 6, +} + +export enum IPScope { + Invalid = -1, + Undefined = 0, + HostLocal = 1, + LinkLocal = 2, + SiteLocal = 3, + Global = 4, + LocalMulticast = 5, + GlobalMulitcast = 6 +} + +let globalScopes = new Set([IPScope.GlobalMulitcast, IPScope.Global]) +let localScopes = new Set([IPScope.SiteLocal, IPScope.LinkLocal, IPScope.LocalMulticast]) + +// IsGlobalScope returns true if scope represents a globally +// routed destination. +export function IsGlobalScope(scope: IPScope): scope is IPScope.GlobalMulitcast | IPScope.Global { + return globalScopes.has(scope); +} + +// IsLocalScope returns true if scope represents a locally +// routed destination. +export function IsLANScope(scope: IPScope): scope is IPScope.SiteLocal | IPScope.LinkLocal | IPScope.LocalMulticast { + return localScopes.has(scope); +} + +// IsLocalhost returns true if scope represents localhost. +export function IsLocalhost(scope: IPScope): scope is IPScope.HostLocal { + return scope === IPScope.HostLocal; +} + +const deniedVerdicts = new Set([ + Verdict.Drop, + Verdict.Block, +]) +// IsDenied returns true if the verdict v represents a +// deny or block decision. +export function IsDenied(v: Verdict): boolean { + return deniedVerdicts.has(v); +} + +export interface CountryInfo { + Code: string; + Name: string; + Center: GeoCoordinates; + Continent: ContinentInfo; +} + +export interface ContinentInfo { + Code: string; + Region: string; + Name: string; +} + +export interface GeoCoordinates { + AccuracyRadius: number; + Latitude: number; + Longitude: number; +} + +export const UnknownLocation: GeoCoordinates = { + AccuracyRadius: 0, + Latitude: 0, + Longitude: 0 +} + +export interface IntelEntity { + // Protocol is the IP protocol used to connect/communicate + // the the described entity. + Protocol: IPProtocol; + // Port is the remote port number used. + Port: number; + // Domain is the domain name of the entity. This may either + // be the domain name used in the DNS request or the + // named returned from reverse PTR lookup. + Domain: string; + // CNAME is a list of CNAMEs that have been used + // to resolve this entity. + CNAME: string[] | null; + // IP is the IP address of the entity. + IP: string; + // IPScope holds the classification of the IP address. + IPScope: IPScope; + // Country holds the country of residence of the IP address. + Country: string; + // ASN holds the number of the autonoumous system that operates + // the IP. + ASN: number; + // ASOrg holds the AS owner name. + ASOrg: string; + // Coordinates contains the geographic coordinates of the entity. + Coordinates: GeoCoordinates | null; + // BlockedByLists holds a list of filter list IDs that + // would have blocked the entity. + BlockedByLists: string[] | null; + // BlockedEntities holds a list of entities that have been + // blocked by filter lists. Those entities can be ASNs, domains, + // CNAMEs, IPs or Countries. + BlockedEntities: string[] | null; + // ListOccurences maps the blocked entity (see BlockedEntities) + // to a list of filter-list IDs that contains it. + ListOccurences: { [key: string]: string[] } | null; +} + +export enum ScopeIdentifier { + IncomingHost = "IH", + IncomingLAN = "IL", + IncomingInternet = "II", + IncomingInvalid = "IX", + PeerHost = "PH", + PeerLAN = "PL", + PeerInternet = "PI", + PeerInvalid = "PX" +} + +export const ScopeTranslation: { [key: string]: string } = { + [ScopeIdentifier.IncomingHost]: "Device-Local Incoming", + [ScopeIdentifier.IncomingLAN]: "LAN Incoming", + [ScopeIdentifier.IncomingInternet]: "Internet Incoming", + [ScopeIdentifier.PeerHost]: "Device-Local Outgoing", + [ScopeIdentifier.PeerLAN]: "LAN Peer-to-Peer", + [ScopeIdentifier.PeerInternet]: "Internet Peer-to-Peer", + [ScopeIdentifier.IncomingInvalid]: "N/A", + [ScopeIdentifier.PeerInvalid]: "N/A", +} + +export interface ProcessContext { + BinaryPath: string; + ProcessName: string; + ProfileName: string; + PID: number; + Profile: string; + Source: string +} + +// Reason justifies the decision on a connection +// verdict. +export interface Reason { + // Msg holds a human readable message of the reason. + Msg: string; + // OptionKey, if available, holds the key of the + // configuration option that caused the verdict. + OptionKey: string; + // Profile holds the profile the option setting has + // been configured in. + Profile: string; + // Context may holds additional data about the reason. + Context: any; +} + +export enum ConnectionType { + Undefined = 0, + IPConnection = 1, + DNSRequest = 2 +} + +export function IsDNSRequest(t: ConnectionType): t is ConnectionType.DNSRequest { + return t === ConnectionType.DNSRequest; +} + +export function IsIPConnection(t: ConnectionType): t is ConnectionType.IPConnection { + return t === ConnectionType.IPConnection; +} + +export interface DNSContext { + Domain: string; + ServedFromCache: boolean; + RequestingNew: boolean; + IsBackup: boolean; + Filtered: boolean; + FilteredEntries: string[], // RR + Question: 'A' | 'AAAA' | 'MX' | 'TXT' | 'SOA' | 'SRV' | 'PTR' | 'NS' | string; + RCode: 'NOERROR' | 'SERVFAIL' | 'NXDOMAIN' | 'REFUSED' | string; + Modified: string; + Expires: string; +} + +export interface TunnelContext { + Path: TunnelNode[]; + PathCost: number; + RoutingAlg: 'default'; +} + +export interface GeoIPInfo { + IP: string; + Country: string; + ASN: number; + ASOwner: string; +} + +export interface TunnelNode { + ID: string; + Name: string; + IPv4?: GeoIPInfo; + IPv6?: GeoIPInfo; + +} + +export interface CertInfo { + Subject: string; + Issuer: string; + AlternateNames: string[]; + NotBefore: dateType; + NotAfter: dateType; +} + +export interface TLSContext { + Version: string; + VersionRaw: number; + SNI: string; + Chain: CertInfo[][]; +} + +export interface Connection extends Record { + // ID is a unique ID for the connection. + ID: string; + // Type defines the connection type. + Type: ConnectionType; + // TLS may holds additional data for the TLS + // session. + TLS: TLSContext | null; + // DNSContext holds additional data about the DNS request for + // this connection. + DNSContext: DNSContext | null; + // TunnelContext holds additional data about the SPN tunnel used for + // the connection. + TunnelContext: TunnelContext | null; + // Scope defines the scope of the connection. It's an somewhat + // weired field that may contain a ScopeIdentifier or a string. + // In case of a string it may eventually be interpreted as a + // domain name. + Scope: ScopeIdentifier | string; + // IPVersion is the version of the IP protocol used. + IPVersion: IPVersion; + // Inbound is true if the connection is incoming to + // hte local system. + Inbound: boolean; + // IPProtocol is the protocol used by the connection. + IPProtocol: IPProtocol; + // LocalIP is the local IP address that is involved into + // the connection. + LocalIP: string; + // LocalIPScope holds the classification of the local IP + // address; + LocalIPScope: IPScope; + // LocalPort is the local port that is involved into the + // connection. + LocalPort: number; + // Entity describes the remote entity that is part of the + // connection. + Entity: IntelEntity; + // Verdict defines the final verdict. + Verdict: Verdict; + // Reason is the reason justifying the verdict of the connection. + Reason: Reason; + // Started holds the number of seconds in UNIX epoch time at which + // the connection was initiated. + Started: number; + // End dholds the number of seconds in UNIX epoch time at which + // the connection was considered terminated. + Ended: number; + // Tunneled is set to true if the connection was tunneled through the + // SPN. + Tunneled: boolean; + // VerdictPermanent is set to true if the connection was marked and + // handed back to the operating system. + VerdictPermanent: boolean; + // Inspecting is set to true if the connection is being inspected. + Inspecting: boolean; + // Encrypted is set to true if the connection is estimated as being + // encrypted. Interpreting this field must be done with care! + Encrypted: boolean; + // Internal is set to true if this connection is done by the Portmaster + // or any associated helper processes/binaries itself. + Internal: boolean; + // ProcessContext holds additional information about the process + // that initated the connection. + ProcessContext: ProcessContext; + // ProfileRevisionCounter is used to track changes to the process + // profile. + ProfileRevisionCounter: number; +} + +export interface ReasonContext { + [key: string]: any; +} diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/portapi.service.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/portapi.service.ts new file mode 100644 index 00000000..4f243ecd --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/portapi.service.ts @@ -0,0 +1,1011 @@ +import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http'; +import { + Inject, + Injectable, + InjectionToken, + isDevMode, + NgZone, +} from '@angular/core'; +import { BehaviorSubject, Observable, Observer, of } from 'rxjs'; +import { + concatMap, + delay, + filter, + map, + retryWhen, + takeWhile, + tap, +} from 'rxjs/operators'; +import { WebSocketSubject } from 'rxjs/webSocket'; +import { + DataReply, + deserializeMessage, + DoneReply, + ImportResult, + InspectedActiveRequest, + isCancellable, + isDataReply, + ProfileImportResult, + Record, + ReplyMessage, + Requestable, + RequestMessage, + RequestType, + RetryableOpts, + retryPipeline, + serializeMessage, + WatchOpts, +} from './portapi.types'; +import { WebsocketService } from './websocket.service'; + +export const PORTMASTER_WS_API_ENDPOINT = new InjectionToken( + 'PortmasterWebsocketEndpoint' +); +export const PORTMASTER_HTTP_API_ENDPOINT = new InjectionToken( + 'PortmasterHttpApiEndpoint' +); + +export const RECONNECT_INTERVAL = 2000; + +let uniqueRequestId = 0; + +interface PendingMethod { + observer: Observer; + request: RequestMessage; +} + +@Injectable() +export class PortapiService { + /** The actual websocket connection, auto-(re)connects on subscription */ + private ws$: WebSocketSubject | null; + + /** used to emit changes to our "connection state" */ + private connectedSubject = new BehaviorSubject(false); + + /** A map to multiplex websocket messages to the actual observer/initator */ + private _streams$ = new Map>>(); + + /** Map to keep track of "still-to-send" requests when we are currently disconnected */ + private _pendingCalls$ = new Map(); + + /** Whether or not we are currently connected. */ + get connected$() { + return this.connectedSubject.asObservable(); + } + + /** @private DEBUGGING ONLY - keeps track of current requests and supports injecting messages */ + readonly activeRequests = new BehaviorSubject<{ + [key: string]: InspectedActiveRequest; + }>({}); + + constructor( + private websocketFactory: WebsocketService, + private ngZone: NgZone, + private http: HttpClient, + @Inject(PORTMASTER_HTTP_API_ENDPOINT) private httpEndpoint: string, + @Inject(PORTMASTER_WS_API_ENDPOINT) private wsEndpoint: string + ) { + // create a new websocket connection that will auto-connect + // on the first subscription and will automatically reconnect + // with consecutive subscribers. + this.ws$ = this.createWebsocket(); + + // no need to keep a reference to the subscription as we're not going + // to unsubscribe ... + this.ws$ + .pipe( + retryWhen((errors) => + errors.pipe( + // use concatMap to keep the errors in order and make sure + // they don't execute in parallel. + concatMap((e, i) => + of(e).pipe( + // We need to forward the error to all streams here because + // due to the retry feature the subscriber below won't see + // any error at all. + tap(() => { + this._streams$.forEach((observer) => observer.error(e)); + this._streams$.clear(); + }), + delay(1000) + ) + ) + ) + ) + ) + .subscribe( + (msg) => { + const observer = this._streams$.get(msg.id); + if (!observer) { + // it's expected that we receive done messages from time to time here + // as portmaster sends a "done" message after we "cancel" a subscription + // and we already remove the observer from _streams$ if the subscription + // is unsubscribed. So just hide that warning message for "done" + if (msg.type !== 'done') { + console.warn( + `Received message for unknown request id ${msg.id} (type=${msg.type})`, + msg + ); + } + return; + } + + // forward the message to the actual stream. + observer.next(msg as ReplyMessage); + }, + console.error, + () => { + // This should actually never happen but if, make sure + // we handle it ... + this._streams$.forEach((observer) => observer.complete()); + this._streams$.clear(); + } + ); + } + + /** Triggers a restart of the portmaster service */ + restartPortmaster(): Observable { + return this.http.post(`${this.httpEndpoint}/v1/core/restart`, undefined, { + observe: 'response', + responseType: 'arraybuffer', + }); + } + + /** Triggers a shutdown of the portmaster service */ + shutdownPortmaster(): Observable { + return this.http.post(`${this.httpEndpoint}/v1/core/shutdown`, undefined, { + observe: 'response', + responseType: 'arraybuffer', + }); + } + + /** Force the portmaster to check for updates */ + checkForUpdates(): Observable { + return this.http.post(`${this.httpEndpoint}/v1/updates/check`, undefined, { + observe: 'response', + responseType: 'arraybuffer', + reportProgress: false, + }); + } + + /** Force a reload of the UI assets */ + reloadUI(): Observable { + return this.http.post(`${this.httpEndpoint}/v1/ui/reload`, undefined, { + observe: 'response', + responseType: 'arraybuffer', + }); + } + + /** Clear DNS cache */ + clearDNSCache(): Observable { + return this.http.post(`${this.httpEndpoint}/v1/dns/clear`, undefined, { + observe: 'response', + responseType: 'arraybuffer', + }); + } + + /** Reset the broadcast notifications state */ + resetBroadcastState(): Observable { + return this.http.post( + `${this.httpEndpoint}/v1/broadcasts/reset-state`, + undefined, + { observe: 'response', responseType: 'arraybuffer' } + ); + } + + /** Re-initialize the SPN */ + reinitSPN(): Observable { + return this.http.post(`${this.httpEndpoint}/v1/spn/reinit`, undefined, { + observe: 'response', + responseType: 'arraybuffer', + }); + } + + /** Cleans up the history database by applying history retention settings */ + cleanupHistory(): Observable { + return this.http.post( + `${this.httpEndpoint}/v1/netquery/history/cleanup`, + undefined, + { observe: 'response', responseType: 'arraybuffer' } + ); + } + + /** Requests a resource from the portmaster as application/json and automatically parses the response body*/ + getResource(resource: string): Observable; + + /** Requests a resource from the portmaster as text */ + getResource(resource: string, type: string): Observable>; + + getResource( + resource: string, + type?: string + ): Observable | any> { + if (type !== undefined) { + return this.http.get(`${this.httpEndpoint}/v1/updates/get/${resource}`, { + headers: new HttpHeaders({ Accept: type }), + observe: 'response', + responseType: 'text', + }); + } + + return this.http.get( + `${this.httpEndpoint}/v1/updates/get/${resource}`, + { + headers: new HttpHeaders({ Accept: 'application/json' }), + responseType: 'json', + } + ); + } + + /** Export one or more settings, either from global settings or a specific profile */ + exportSettings( + keys: string[], + from: 'global' | string = 'global' + ): Observable { + return this.http.post( + `${this.httpEndpoint}/v1/sync/settings/export`, + { + from, + keys, + }, + { + headers: new HttpHeaders({ Accept: 'text/yaml' }), + responseType: 'text', + observe: 'body', + } + ); + } + + /** Validate a settings import for a given target */ + validateSettingsImport( + blob: string | Blob, + target: string | 'global' = 'global', + mimeType: string = 'text/yaml' + ): Observable { + return this.http.post( + `${this.httpEndpoint}/v1/sync/settings/import`, + { + target, + rawExport: blob.toString(), + rawMime: mimeType, + validateOnly: true, + } + ); + } + + /** Import settings into a given target */ + importSettings( + blob: string | Blob, + target: string | 'global' = 'global', + mimeType: string = 'text/yaml', + reset = false, + allowUnknown = false + ): Observable { + return this.http.post( + `${this.httpEndpoint}/v1/sync/settings/import`, + { + target, + rawExport: blob.toString(), + rawMime: mimeType, + validateOnly: false, + reset, + allowUnknown, + } + ); + } + + /** Import a profile */ + importProfile( + blob: string | Blob, + mimeType: string = 'text/yaml', + reset = false, + allowUnknown = false, + allowReplaceProfiles = false + ): Observable { + return this.http.post( + `${this.httpEndpoint}/v1/sync/profile/import`, + { + rawExport: blob.toString(), + rawMime: mimeType, + validateOnly: false, + reset, + allowUnknown, + allowReplaceProfiles, + } + ); + } + + /** Import a profile */ + validateProfileImport( + blob: string | Blob, + mimeType: string = 'text/yaml' + ): Observable { + return this.http.post( + `${this.httpEndpoint}/v1/sync/profile/import`, + { + rawExport: blob.toString(), + rawMime: mimeType, + validateOnly: true, + } + ); + } + + /** Export one or more settings, either from global settings or a specific profile */ + exportProfile(id: string): Observable { + return this.http.post( + `${this.httpEndpoint}/v1/sync/profile/export`, + { + id, + }, + { + headers: new HttpHeaders({ Accept: 'text/yaml' }), + responseType: 'text', + observe: 'body', + } + ); + } + + /** Merge multiple profiles into one primary profile. */ + mergeProfiles( + name: string, + primary: string, + secondaries: string[] + ): Observable { + return this.http + .post<{ new: string }>(`${this.httpEndpoint}/v1/profile/merge`, { + name: name, + to: primary, + from: secondaries, + }) + .pipe(map((response) => response.new)); + } + + /** + * Injects an event into a module to trigger certain backend + * behavior. + * + * @deprecated - Use the HTTP API instead. + * + * @param module The name of the module to inject + * @param kind The event kind to inject + */ + bridgeAPI(call: string, method: string): Observable { + return this.create(`api:${call}`, { + Method: method, + }).pipe(map(() => { })); + } + + /** + * Flushes all pending method calls that have been collected + * while we were not connected to the portmaster API. + */ + private _flushPendingMethods() { + const count = this._pendingCalls$.size; + try { + this._pendingCalls$.forEach((req, key) => { + // It's fine if we throw an error here! + this.ws$!.next(req.request); + this._streams$.set(req.request.id, req.observer); + this._pendingCalls$.delete(key); + }); + } catch (err) { + // we failed to send the pending calls because the + // websocket connection just broke. + console.error( + `Failed to flush pending calls, ${this._pendingCalls$.size} left: `, + err + ); + } + + console.log(`Successfully flushed all (${count}) pending calles`); + } + + /** + * Allows to inspect currently active requests. + */ + inspectActiveRequests(): { [key: string]: InspectedActiveRequest } { + return this.activeRequests.getValue(); + } + + /** + * Loads a database entry. The returned observable completes + * after the entry has been loaded. + * + * @param key The database key of the entry to load. + */ + get(key: string): Observable { + return this.request('get', { key }).pipe(map((res) => res.data)); + } + + /** + * Searches for multiple database entries at once. Each entry + * is streams via the returned observable. The observable is + * closed after the last entry has been published. + * + * @param query The query used to search the database. + */ + query(query: string): Observable> { + return this.request('query', { query }); + } + + /** + * Subscribes for updates on entries of the selected query. + * + * @param query The query use to subscribe. + */ + sub( + query: string, + opts: RetryableOpts = {} + ): Observable> { + return this.request('sub', { query }).pipe(retryPipeline(opts)); + } + + /** + * Subscribes for updates on entries of the selected query and + * ensures entries are stream once upon subscription. + * + * @param query The query use to subscribe. + * @todo(ppacher): check what a ok/done message mean here. + */ + qsub( + query: string, + opts?: RetryableOpts + ): Observable>; + qsub( + query: string, + opts: RetryableOpts, + _: { forwardDone: true } + ): Observable | DoneReply>; + qsub( + query: string, + opts: RetryableOpts = {}, + { forwardDone }: { forwardDone?: true } = {} + ): Observable> { + return this.request('qsub', { query }, { forwardDone }).pipe( + retryPipeline(opts) + ); + } + + /** + * Creates a new database entry. + * + * @warn create operations do not validate the type of data + * to be overwritten (for keys that does already exist). + * Use {@function insert} for more validation. + * + * @param key The database key for the entry. + * @param data The actual data for the entry. + */ + create(key: string, data: any): Observable { + data = this.stripMeta(data); + return this.request('create', { key, data }).pipe(map(() => { })); + } + + /** + * Updates an existing entry. + * + * @param key The database key for the entry + * @param data The actual, updated entry data. + */ + update(key: string, data: any): Observable { + data = this.stripMeta(data); + return this.request('update', { key, data }).pipe(map(() => { })); + } + + /** + * Creates a new database entry. + * + * @param key The database key for the entry. + * @param data The actual data for the entry. + * @todo(ppacher): check what's different to create(). + */ + insert(key: string, data: any): Observable { + data = this.stripMeta(data); + return this.request('insert', { key, data }).pipe(map(() => { })); + } + + /** + * Deletes an existing database entry. + * + * @param key The key of the database entry to delete. + */ + delete(key: string): Observable { + return this.request('delete', { key }).pipe(map(() => { })); + } + + /** + * Watch a database key for modifications. If the + * websocket connection is lost or an error is returned + * watch will automatically retry after retryDelay + * milliseconds. It stops retrying to watch key once + * maxRetries is exceeded. The returned observable completes + * when the watched key is deleted. + * + * @param key The database key to watch + * @param opts.retryDelay Number of milliseconds to wait + * between retrying the request. Defaults to 1000 + * @param opts.maxRetries Maximum number of tries before + * giving up. Defaults to Infinity + * @param opts.ingoreNew Whether or not `new` notifications + * will be ignored. Defaults to false + * @param opts.ignoreDelete Whether or not "delete" notification + * will be ignored (and replaced by null) + * @param forwardDone: Whether or not the "done" message should be forwarded + */ + watch(key: string, opts?: WatchOpts): Observable; + watch( + key: string, + opts?: WatchOpts & { ignoreDelete: true } + ): Observable; + watch( + key: string, + opts: WatchOpts, + _: { forwardDone: true } + ): Observable; + watch( + key: string, + opts: WatchOpts & { ignoreDelete: true }, + _: { forwardDone: true } + ): Observable; + watch( + key: string, + opts: WatchOpts = {}, + { forwardDone }: { forwardDone?: boolean } = {} + ): Observable { + return this.qsub(key, opts, { forwardDone } as any).pipe( + filter((reply) => reply.type !== 'done' || forwardDone === true), + filter((reply) => reply.type === 'done' || reply.key === key), + takeWhile((reply) => opts.ignoreDelete || reply.type !== 'del'), + filter((reply) => { + return !opts.ingoreNew || reply.type !== 'new'; + }), + map((reply) => { + if (reply.type === 'del') { + return null; + } + + if (reply.type === 'done') { + return reply; + } + return reply.data; + }) + ); + } + + watchAll( + query: string, + opts?: RetryableOpts + ): Observable { + return new Observable((observer) => { + let values: T[] = []; + let keys: string[] = []; + let doneReceived = false; + + const sub = this.request( + 'qsub', + { query }, + { forwardDone: true } + ).subscribe({ + next: (value) => { + if ((value as any).type === 'done') { + doneReceived = true; + observer.next(values); + return; + } + + if (!doneReceived) { + values.push(value.data); + keys.push(value.key); + return; + } + + const idx = keys.findIndex((k) => k === value.key); + switch (value.type) { + case 'new': + if (idx < 0) { + values.push(value.data); + keys.push(value.key); + } else { + /* + const existing = values[idx]._meta!; + const existingTs = existing.Modified || existing.Created; + const newTs = (value.data as Record)?._meta?.Modified || (value.data as Record)?._meta?.Created || 0; + + console.log(`Comparing ${newTs} against ${existingTs}`); + + if (newTs > existingTs) { + console.log(`New record is ${newTs - existingTs} seconds newer`); + values[idx] = value.data; + } else { + return; + } + */ + values[idx] = value.data; + } + break; + case 'del': + if (idx >= 0) { + keys.splice(idx, 1); + values.splice(idx, 1); + } + break; + case 'upd': + if (idx >= 0) { + values[idx] = value.data; + } + break; + } + + observer.next(values); + }, + error: (err) => { + observer.error(err); + }, + complete: () => { + observer.complete(); + }, + }); + + return () => { + sub.unsubscribe(); + }; + }).pipe(retryPipeline(opts)); + } + + /** + * Close the current websocket connection. A new subscription + * will _NOT_ trigger a reconnect. + */ + close() { + if (!this.ws$) { + return; + } + + this.ws$.complete(); + this.ws$ = null; + } + + request( + method: M, + attrs: Partial>, + { forwardDone }: { forwardDone?: boolean } = {} + ): Observable> { + return new Observable((observer) => { + const id = `${++uniqueRequestId}`; + if (!this.ws$) { + observer.error('No websocket connection'); + return; + } + + let shouldCancel = isCancellable(method); + let unsub: () => RequestMessage | null = () => { + if (shouldCancel) { + return { + id: id, + type: 'cancel', + }; + } + + return null; + }; + + const request: any = { + ...attrs, + id: id, + type: method, + }; + + let inspected: InspectedActiveRequest = { + type: method, + messagesReceived: 0, + observer: observer, + payload: request, + lastData: null, + lastKey: '', + }; + + if (isDevMode()) { + this.activeRequests.next({ + ...this.inspectActiveRequests(), + [id]: inspected, + }); + } + + let stream$: Observable> = this.multiplex( + request, + unsub + ); + if (isDevMode()) { + // in development mode we log all replys for the different + // methods. This also includes updates to subscriptions. + stream$ = stream$.pipe( + tap( + (msg) => { }, + //msg => console.log(`[portapi] reply for ${method} ${id}: `, msg), + (err) => console.error(`[portapi] error in ${method} ${id}: `, err) + ) + ); + } + + const subscription = stream$?.subscribe({ + next: (data) => { + inspected.messagesReceived++; + + // in all cases, an `error` message type + // terminates the data flow. + if (data.type === 'error') { + console.error(data.message, inspected); + shouldCancel = false; + + observer.error(data.message); + return; + } + + if ( + method === 'create' || + method === 'update' || + method === 'insert' || + method === 'delete' + ) { + // for data-manipulating methods success + // ends the stream. + if (data.type === 'success') { + observer.next(); + observer.complete(); + return; + } + } + + if (method === 'query' || method === 'sub' || method === 'qsub') { + if (data.type === 'warning') { + console.warn(data.message); + return; + } + + // query based methods send `done` once all + // results are sent at least once. + if (data.type === 'done') { + if (method === 'query') { + // done ends the query but does not end sub or qsub + shouldCancel = false; + observer.complete(); + return; + } + + if (!!forwardDone) { + // A done message in qsub does not actually represent + // a DataReply but we still want to forward that. + observer.next(data as any); + } + return; + } + } + + if (!isDataReply(data)) { + console.error( + `Received unexpected message type ${data.type} in a ${method} operation` + ); + return; + } + + inspected.lastData = data.data; + inspected.lastKey = data.key; + + observer.next(data); + + // for a `get` method the first `ok` message + // also marks the end of the stream. + if (method === 'get' && data.type === 'ok') { + shouldCancel = false; + observer.complete(); + } + }, + error: (err) => { + console.error(err, attrs); + observer.error(err); + }, + complete: () => { + observer.complete(); + }, + }); + + if (isDevMode()) { + // make sure we remove the "active" request when the subscription + // goes down + subscription.add(() => { + const active = this.inspectActiveRequests(); + delete active[request.id]; + this.activeRequests.next(active); + }); + } + + return () => { + subscription.unsubscribe(); + }; + }); + } + + private multiplex( + req: RequestMessage, + cancel: (() => RequestMessage | null) | null + ): Observable { + return new Observable((observer) => { + if (this.connectedSubject.getValue()) { + // Try to directly send the request to the backend + this._streams$.set(req.id, observer); + this.ws$!.next(req); + } else { + // in case of an error we just add the request as + // "pending" and wait for the connection to be + // established. + console.warn( + `Failed to send request ${req.id}:${req.type}, marking as pending ...` + ); + this._pendingCalls$.set(req.id, { + request: req, + observer: observer, + }); + } + + return () => { + // Try to cancel the request but ingore + // any errors here. + try { + if (cancel !== null) { + const cancelMsg = cancel(); + if (!!cancelMsg) { + this.ws$!.next(cancelMsg); + } + } + } catch (err) { } + + this._pendingCalls$.delete(req.id); + this._streams$.delete(req.id); + }; + }); + } + + /** + * Inject a message into a PortAPI stream. + * + * @param id The request ID to inject msg into. + * @param msg The message to inject. + */ + _injectMessage(id: string, msg: DataReply) { + // we are using runTask here so change-detection is + // triggered as needed + this.ngZone.runTask(() => { + const req = this.activeRequests.getValue()[id]; + if (!req) { + return; + } + + req.observer.next(msg as DataReply); + }); + } + + /** + * Injects a 'ok' type message + * + * @param id The ID of the request to inject into + * @param data The data blob to inject + * @param key [optional] The key of the entry to inject + */ + _injectData(id: string, data: any, key: string = '') { + this._injectMessage(id, { type: 'ok', data: data, key, id: id }); + } + + /** + * Patches the last message received on id by deeply merging + * data and re-injects that message. + * + * @param id The ID of the request + * @param data The patch to apply and reinject + */ + _patchLast(id: string, data: any) { + const req = this.activeRequests.getValue()[id]; + if (!req || !req.lastData) { + return; + } + + const newPayload = mergeDeep({}, req.lastData, data); + this._injectData(id, newPayload, req.lastKey); + } + + private stripMeta(obj: T): T { + let copy = { + ...obj, + _meta: undefined, + }; + return copy; + } + + /** + * Creates a new websocket subject and configures appropriate serializer + * and deserializer functions for PortAPI. + * + * @private + */ + private createWebsocket(): WebSocketSubject { + return this.websocketFactory.createConnection< + ReplyMessage | RequestMessage + >({ + url: this.wsEndpoint, + serializer: (msg) => { + try { + return serializeMessage(msg); + } catch (err) { + console.error('serialize message', err); + return { + type: 'error', + }; + } + }, + // deserializeMessage also supports RequestMessage so cast as any + deserializer: ((msg: any) => { + try { + const res = deserializeMessage(msg); + return res; + } catch (err) { + console.error('deserialize message', err); + return { + type: 'error', + }; + } + }), + binaryType: 'arraybuffer', + openObserver: { + next: () => { + console.log('[portapi] connection to portmaster established'); + this.connectedSubject.next(true); + this._flushPendingMethods(); + }, + }, + closeObserver: { + next: () => { + console.log('[portapi] connection to portmaster closed'); + this.connectedSubject.next(false); + }, + }, + closingObserver: { + next: () => { + console.log('[portapi] connection to portmaster closing'); + }, + }, + }); + } +} + +// Counts the number of "truthy" datafields in obj. +function countTruthyDataFields(obj: { [key: string]: any }): number { + let count = 0; + Object.keys(obj).forEach((key) => { + let value = obj[key]; + if (!!value) { + count++; + } + }); + return count; +} + +function isObject(item: any): item is Object { + return item && typeof item === 'object' && !Array.isArray(item); +} + +export function mergeDeep(target: any, ...sources: any): any { + if (!sources.length) return target; + const source = sources.shift(); + + if (isObject(target) && isObject(source)) { + for (const key in source) { + if (isObject(source[key])) { + if (!target[key]) Object.assign(target, { [key]: {} }); + mergeDeep(target[key], source[key]); + } else { + Object.assign(target, { [key]: source[key] }); + } + } + } + + return mergeDeep(target, ...sources); +} diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/portapi.types.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/portapi.types.ts new file mode 100644 index 00000000..349c7b9f --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/portapi.types.ts @@ -0,0 +1,453 @@ +import { iif, MonoTypeOperatorFunction, of, Subscriber, throwError } from 'rxjs'; +import { concatMap, delay, retryWhen } from 'rxjs/operators'; + +/** +* ReplyType contains all possible message types of a reply. +*/ +export type ReplyType = 'ok' + | 'upd' + | 'new' + | 'del' + | 'success' + | 'error' + | 'warning' + | 'done'; + +/** +* RequestType contains all possible message types of a request. +*/ +export type RequestType = 'get' + | 'query' + | 'sub' + | 'qsub' + | 'create' + | 'update' + | 'insert' + | 'delete' + | 'cancel'; + +// RecordMeta describes the meta-data object that is part of +// every API resource. +export interface RecordMeta { + // Created hold a unix-epoch timestamp when the record has been + // created. + Created: number; + // Deleted hold a unix-epoch timestamp when the record has been + // deleted. + Deleted: number; + // Expires hold a unix-epoch timestamp when the record has been + // expires. + Expires: number; + // Modified hold a unix-epoch timestamp when the record has been + // modified last. + Modified: number; + // Key holds the database record key. + Key: string; +} + +export interface Process extends Record { + Name: string; + UserID: number; + UserName: string; + UserHome: string; + Pid: number; + Pgid: number; + CreatedAt: number; + ParentPid: number; + ParentCreatedAt: number; + Path: string; + ExecName: string; + Cwd: string; + CmdLine: string; + FirstArg: string; + Env: { + [key: string]: string + } | null; + Tags: { + Key: string; + Value: string; + }[] | null; + MatchingPath: string; + PrimaryProfileID: string; + FirstSeen: number; + LastSeen: number; + Error: string; + ExecHashes: { + [key: string]: string + } | null; +} + +// Record describes the base record structure of all API resources. +export interface Record { + _meta?: RecordMeta; +} + +/** +* All possible MessageType that are available in PortAPI. +*/ +export type MessageType = RequestType | ReplyType; + +/** +* BaseMessage describes the base message type that is exchanged +* via PortAPI. +*/ +export interface BaseMessage { + // ID of the request. Used to correlated (multiplex) requests and + // responses across a single websocket connection. + id: string; + // Type is the request/response message type. + type: M; +} + +/** +* DoneReply marks the end of a PortAPI stream. +*/ +export interface DoneReply extends BaseMessage<'done'> { } + +/** +* DataReply is either sent once as a result on a `get` request or +* is sent multiple times in the course of a PortAPI stream. +*/ +export interface DataReply extends BaseMessage<'ok' | 'upd' | 'new' | 'del'> { + // Key is the database key including the database prefix. + key: string; + // Data is the actual data of the entry. + data: T; +} + +/** + * Returns true if d is a DataReply message type. + * + * @param d The reply message to check + */ +export function isDataReply(d: ReplyMessage): d is DataReply { + return d.type === 'ok' + || d.type === 'upd' + || d.type === 'new' + || d.type === 'del'; + //|| d.type === 'done'; // done is actually not correct +} + +/** +* SuccessReply is used to mark an operation as successfully. It does not carry any +* data. Think of it as a "201 No Content" in HTTP. +*/ +export interface SuccessReply extends BaseMessage<'success'> { } + +/** +* ErrorReply describes an error that happened while processing a +* request. Note that an `error` type message may be sent for single +* and response-stream requests. In case of a stream the `error` type +* message marks the end of the stream. See WarningReply for a simple +* warning message that can be transmitted via PortAPI. +*/ +export interface ErrorReply extends BaseMessage<'error'> { + // Message is the error message from the backend. + message: string; +} + +/** +* WarningReply contains a warning message that describes an error +* condition encountered when processing a single entitiy of a +* response stream. In contrast to `error` type messages, a `warning` +* can only occure during data streams and does not end the stream. +*/ +export interface WarningReply extends BaseMessage<'warning'> { + // Message describes the warning/error condition the backend + // encountered. + message: string; +} + +/** +* QueryRequest defines the payload for `query`, `sub` and `qsub` message +* types. The result of a query request is always a stream of responses. +* See ErrorReply, WarningReply and DoneReply for more information. +*/ +export interface QueryRequest extends BaseMessage<'query' | 'sub' | 'qsub'> { + // Query is the query for the database. + query: string; +} + +/** +* KeyRequests defines the payload for a `get` or `delete` request. Those +* message type only carry the key of the database entry to delete. Note that +* `delete` can only return a `success` or `error` type message while `get` will +* receive a `ok` or `error` type message. +*/ +export interface KeyRequest extends BaseMessage<'delete' | 'get'> { + // Key is the database entry key. + key: string; +} + + +/** +* DataRequest is used during create, insert or update operations. +* TODO(ppacher): check what's the difference between create and insert, +* both seem to error when trying to create a new entry. +*/ +export interface DataRequest extends BaseMessage<'update' | 'create' | 'insert'> { + // Key is the database entry key. + key: string; + // Data is the data to store. + data: T; +} + +/** + * CancelRequest can be sent on stream operations to early-abort the request. + */ +export interface CancelRequest extends BaseMessage<'cancel'> { } + +/** +* ReplyMessage is a union of all reply message types. +*/ +export type ReplyMessage = DataReply + | DoneReply + | SuccessReply + | WarningReply + | ErrorReply; + +/** +* RequestMessage is a union of all request message types. +*/ +export type RequestMessage = QueryRequest + | KeyRequest + | DataRequest + | CancelRequest; + +/** +* Requestable can be used to accept only properties that match +* the request message type M. +*/ +export type Requestable = RequestMessage & { type: M }; + +/** + * Returns true if m is a cancellable message type. + * + * @param m The message type to check. + */ +export function isCancellable(m: MessageType): boolean { + switch (m) { + case 'qsub': + case 'sub': + return true; + default: + return false; + } +} + +/** + * Reflects a currently in-flight PortAPI request. Used to + * intercept and mangle with responses. + */ +export interface InspectedActiveRequest { + // The type of request. + type: RequestType; + // The actual request payload. + // @todo(ppacher): typings + payload: any; + // The request observer. Use to inject data + // or complete/error the subscriber. Use with + // care! + observer: Subscriber>; + // Counter for the number of messages received + // for this request. + messagesReceived: number; + // The last data received on the request + lastData: any; + // The last key received on the request + lastKey: string; +} + +export interface RetryableOpts { + // A delay in milliseconds before retrying an operation. + retryDelay?: number; + // The maximum number of retries. + maxRetries?: number; +} + +export interface ProfileImportResult extends ImportResult { + replacesProfiles: string[]; +} + +export interface ImportResult { + restartRequired: boolean; + replacesExisting: boolean; + containsUnknown: boolean; +} + +/** + * Returns a RxJS operator function that implements a retry pipeline + * with a configurable retry delay and an optional maximum retry count. + * If maxRetries is reached the last error captured is thrown. + * + * @param opts Configuration options for the retryPipeline. + * see {@type RetryableOpts} for more information. + */ +export function retryPipeline({ retryDelay, maxRetries }: RetryableOpts = {}): MonoTypeOperatorFunction { + return retryWhen(errors => errors.pipe( + // use concatMap to keep the errors in order and make sure + // they don't execute in parallel. + concatMap((e, i) => + iif( + // conditional observable seletion, throwError if i > maxRetries + // or a retryDelay otherwise + () => i > (maxRetries || Infinity), + throwError(() => e), + of(e).pipe(delay(retryDelay || 1000)) + ) + ) + )) +} + +export interface WatchOpts extends RetryableOpts { + // Whether or not `new` updates should be filtered + // or let through. See {@method PortAPI.watch} for + // more information. + ingoreNew?: boolean; + + ignoreDelete?: boolean; +} + + +/** +* Serializes a request or reply message into it's wire format. +* +* @param msg The request or reply messsage to serialize +*/ +export function serializeMessage(msg: RequestMessage | ReplyMessage): any { + if (msg === undefined) { + return undefined; + } + + let blob = `${msg.id}|${msg.type}`; + + switch (msg.type) { + case 'done': // reply + case 'success': // reply + case 'cancel': // request + break; + + case 'error': // reply + case 'warning': // reply + blob += `|${msg.message}` + break; + + case 'ok': // reply + case 'upd': // reply + case 'new': // reply + case 'insert': // request + case 'update': // request + case 'create': // request + blob += `|${msg.key}|J${JSON.stringify(msg.data)}` + break; + + + case 'del': // reply + case 'get': // request + case 'delete': // request + blob += `|${msg.key}` + break; + + case 'query': // request + case 'sub': // request + case 'qsub': // request + blob += `|query ${msg.query}` + break; + + default: + // We need (msg as any) here because typescript knows that we covered + // all possible values above and that .type can never be something else. + // Still, we want to guard against unexpected portmaster message + // types. + console.error(`Unknown message type ${(msg as any).type}`); + } + + return blob; +} + +/** +* Deserializes (loads) a PortAPI message from a WebSocket message event. +* +* @param event The WebSocket MessageEvent to parse. +*/ +export function deserializeMessage(event: MessageEvent): RequestMessage | ReplyMessage { + let data: string; + + if (typeof event.data !== 'string') { + data = new TextDecoder("utf-8").decode(event.data) + } else { + data = event.data; + } + + const parts = data.split("|"); + + if (parts.length < 2) { + throw new Error(`invalid number of message parts, expected 3-4 but got ${parts.length}`); + } + + const id = parts[0]; + const type = parts[1] as MessageType; + + var msg: Partial = { + id, + type, + } + + if (parts.length > 4) { + parts[3] = parts.slice(3).join('|') + } + + switch (msg.type) { + case 'done': // reply + case 'success': // reply + case 'cancel': // request + break; + + case 'error': // reply + case 'warning': // reply + msg.message = parts[2]; + break; + + case 'ok': // reply + case 'upd': // reply + case 'new': // reply + case 'insert': // request + case 'update': // request + case 'create': // request + msg.key = parts[2]; + try { + if (parts[3][0] === 'J') { + msg.data = JSON.parse(parts[3].slice(1)); + } else { + msg.data = parts[3]; + } + } catch (e) { + console.log(e, data) + } + break; + + case 'del': // reply + case 'get': // request + case 'delete': // request + msg.key = parts[2]; + break; + + case 'query': // request + case 'sub': // request + case 'qsub': // request + msg.query = parts[2]; + if (msg.query.startsWith("query ")) { + msg.query = msg.query.slice(6); + } + break; + + default: + // We need (msg as any) here because typescript knows that we covered + // all possible values above and that .type can never be something else. + // Still, we want to guard against unexpected portmaster message + // types. + console.error(`Unknown message type ${(msg as any).type}`); + } + + return msg as (ReplyMessage | RequestMessage); // it's not partitial anymore +} diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/spn.service.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/spn.service.ts new file mode 100644 index 00000000..fc0a6047 --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/spn.service.ts @@ -0,0 +1,171 @@ +import { HttpClient, HttpParams, HttpResponse } from "@angular/common/http"; +import { Inject, Injectable } from "@angular/core"; +import { BehaviorSubject, Observable, of } from "rxjs"; +import { filter, map, share, switchMap } from "rxjs/operators"; +import { FeatureID } from "./features"; +import { PORTMASTER_HTTP_API_ENDPOINT, PortapiService } from './portapi.service'; +import { Feature, Pin, SPNStatus, UserProfile } from "./spn.types"; + +@Injectable({ providedIn: 'root' }) +export class SPNService { + + /** Emits the SPN status whenever it changes */ + status$: Observable; + + profile$ = this.watchProfile() + .pipe( + share({ connector: () => new BehaviorSubject(undefined) }), + filter(val => val !== undefined) + ) as Observable; + + private pins$: Observable; + + constructor( + private portapi: PortapiService, + private http: HttpClient, + @Inject(PORTMASTER_HTTP_API_ENDPOINT) private httpAPI: string, + ) { + this.status$ = this.portapi.watch('runtime:spn/status', { ignoreDelete: true }) + .pipe( + share({ connector: () => new BehaviorSubject(null) }), + filter(val => val !== null), + ) + + this.pins$ = this.status$ + .pipe( + switchMap(status => { + if (status.Status !== "disabled") { + return this.portapi.watchAll("map:main/", { retryDelay: 50000 }) + } + + return of([] as Pin[]); + }), + share({ connector: () => new BehaviorSubject(undefined) }), + filter(val => val !== undefined) + ) as Observable; + } + + /** + * Watches all pins of the "main" SPN map. + */ + watchPins(): Observable { + return this.pins$; + } + + /** + * Encodes a unicode string to base64. + * See https://developer.mozilla.org/en-US/docs/Web/API/btoa + * and https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings + */ + b64EncodeUnicode(str: string): string { + return window.btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) { + return String.fromCharCode(parseInt(p1, 16)) + })) + } + + /** + * Logs into the SPN user account + */ + login({ username, password }: { username: string, password: string }): Observable> { + return this.http.post(`${this.httpAPI}/v1/spn/account/login`, undefined, { + headers: { + Authorization: `Basic ${this.b64EncodeUnicode(username + ':' + password)}` + }, + responseType: 'text', + observe: 'response' + }); + } + + /** + * Log out of the SPN user account + * + * @param purge Whether or not the portmaster should keep user/device information for the next login + */ + logout(purge = false): Observable> { + let params = new HttpParams(); + if (!!purge) { + params = params.set("purge", "true") + } + return this.http.delete(`${this.httpAPI}/v1/spn/account/logout`, { + params, + responseType: 'text', + observe: 'response' + }) + } + + watchEnabledFeatures(): Observable<(Feature & { enabled: boolean })[]> { + return this.profile$ + .pipe( + switchMap(profile => { + return this.loadFeaturePackages() + .pipe( + map(features => { + return features.map(feature => { + // console.log(feature, profile?.current_plan?.feature_ids) + return { + ...feature, + enabled: feature.RequiredFeatureID === FeatureID.None || profile?.current_plan?.feature_ids?.includes(feature.RequiredFeatureID) || false, + } + }) + }) + ) + }) + ); + } + + /** Returns a list of all feature packages */ + loadFeaturePackages(): Observable { + return this.http.get<{ Features: Feature[] }>(`${this.httpAPI}/v1/account/features`) + .pipe( + map(response => response.Features.map(feature => { + return { + ...feature, + IconURL: `${this.httpAPI}/v1/account/features/${feature.ID}/icon`, + } + })) + ); + } + + /** + * Returns the current SPN user profile. + * + * @param refresh Whether or not the user profile should be refreshed from the ticket agent + * @returns + */ + userProfile(refresh = false): Observable { + let params = new HttpParams(); + if (!!refresh) { + params = params.set("refresh", true) + } + return this.http.get(`${this.httpAPI}/v1/spn/account/user/profile`, { + params + }); + } + + /** + * Watches the user profile. It will emit null if there is no profile available yet. + */ + watchProfile(): Observable { + let hasSent = false; + return this.portapi.watch('core:spn/account/user', { ignoreDelete: true }, { forwardDone: true }) + .pipe( + filter(result => { + if ('type' in result && result.type === 'done') { + if (hasSent) { + return false; + } + } + + return true + }), + map(result => { + hasSent = true; + if ('type' in result) { + return null; + } + + return result; + }) + ); + } +} diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/spn.types.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/spn.types.ts new file mode 100644 index 00000000..b2e7caaf --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/spn.types.ts @@ -0,0 +1,104 @@ +import { FeatureID } from './features'; +import { CountryInfo, GeoCoordinates, IntelEntity } from './network.types'; +import { Record } from './portapi.types'; + +export interface SPNStatus extends Record { + Status: 'failed' | 'disabled' | 'connecting' | 'connected'; + HomeHubID: string; + HomeHubName: string; + ConnectedIP: string; + ConnectedTransport: string; + ConnectedCountry: CountryInfo | null; + ConnectedSince: string | null; +} + +export interface Pin extends Record { + ID: string; + Name: string; + FirstSeen: string; + EntityV4?: IntelEntity | null; + EntityV6?: IntelEntity | null; + States: string[]; + SessionActive: boolean; + HopDistance: number; + ConnectedTo: { + [key: string]: Lane, + }; + Route: string[] | null; + VerifiedOwner: string; +} + +export interface Lane { + HubID: string; + Capacity: number; + Latency: number; +} + +export function getPinCoords(p: Pin): GeoCoordinates | null { + if (p.EntityV4 && p.EntityV4.Coordinates) { + return p.EntityV4.Coordinates; + } + return p.EntityV6?.Coordinates || null; +} + +export interface Device { + name: string; + id: string; +} + +export interface Subscription { + ends_at: string; + state: 'manual' | 'active' | 'cancelled'; + next_billing_date: string; + payment_provider: string; +} + +export interface Plan { + name: string; + amount: number; + months: number; + renewable: boolean; + feature_ids: FeatureID[]; +} + +export interface View { + Message: string; + ShowAccountData: boolean; + ShowAccountButton: boolean; + ShowLoginButton: boolean; + ShowRefreshButton: boolean; + ShowLogoutButton: boolean; +} + +export interface UserProfile extends Record { + username: string; + state: string; + balance: number; + device: Device | null; + subscription: Subscription | null; + current_plan: Plan | null; + next_plan: Plan | null; + view: View | null; + LastNotifiedOfEnd?: string; + LoggedInAt?: string; +} + +export interface Package { + Name: string; + HexColor: string; +} + +export interface Feature { + ID: string; + Name: string; + ConfigKey: string; + ConfigScope: string; + RequiredFeatureID: FeatureID; + InPackage: Package | null; + Comment: string; + Beta?: boolean; + ComingSoon?: boolean; + + // does not come from the PM API but is set by SPNService + IconURL: string; +} diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/utils.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/utils.ts new file mode 100644 index 00000000..80b97573 --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/utils.ts @@ -0,0 +1,13 @@ + +export function deepClone(o?: T | null): T { + if (o === null || o === undefined) { + return null as any as T; + } + + let _out: T = (Array.isArray(o) ? [] : {}) as T; + for (let _key in (o as T)) { + let v = o[_key]; + _out[_key] = (typeof v === "object") ? deepClone(v) : v; + } + return _out as T; +} diff --git a/desktop/angular/projects/safing/portmaster-api/src/lib/websocket.service.ts b/desktop/angular/projects/safing/portmaster-api/src/lib/websocket.service.ts new file mode 100644 index 00000000..c42efa8d --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/lib/websocket.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { webSocket, WebSocketSubject, WebSocketSubjectConfig } from 'rxjs/webSocket'; + +@Injectable() +export class WebsocketService { + constructor() { } + + /** + * createConnection creates a new websocket connection using opts. + * + * @param opts Options for the websocket connection. + */ + createConnection(opts: WebSocketSubjectConfig): WebSocketSubject { + return webSocket(opts); + } +} + diff --git a/desktop/angular/projects/safing/portmaster-api/src/public-api.ts b/desktop/angular/projects/safing/portmaster-api/src/public-api.ts new file mode 100644 index 00000000..9097761e --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/public-api.ts @@ -0,0 +1,22 @@ +/* + * Public API Surface of portmaster-api + */ + +export * from './lib/app-profile.service'; +export * from './lib/app-profile.types'; +export * from './lib/config.service'; +export * from './lib/config.types'; +export * from './lib/core.types'; +export * from './lib/debug-api.service'; +export * from './lib/features'; +export * from './lib/meta-api.service'; +export * from './lib/module'; +export * from './lib/netquery.service'; +export * from './lib/network.types'; +export * from './lib/portapi.service'; +export * from './lib/portapi.types'; +export * from './lib/spn.service'; +export * from './lib/spn.types'; +export * from './lib/utils'; +export * from './lib/websocket.service'; + diff --git a/desktop/angular/projects/safing/portmaster-api/src/test.ts b/desktop/angular/projects/safing/portmaster-api/src/test.ts new file mode 100644 index 00000000..43808367 --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/src/test.ts @@ -0,0 +1,15 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js'; +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), +); diff --git a/desktop/angular/projects/safing/portmaster-api/tsconfig.lib.json b/desktop/angular/projects/safing/portmaster-api/tsconfig.lib.json new file mode 100644 index 00000000..c9f14589 --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/tsconfig.lib.json @@ -0,0 +1,16 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "../../../out-tsc/lib", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/test.ts", + "testing/**/*", + "**/*.spec.ts" + ] +} diff --git a/desktop/angular/projects/safing/portmaster-api/tsconfig.lib.prod.json b/desktop/angular/projects/safing/portmaster-api/tsconfig.lib.prod.json new file mode 100644 index 00000000..71b135f6 --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/tsconfig.lib.prod.json @@ -0,0 +1,7 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, +} diff --git a/desktop/angular/projects/safing/portmaster-api/tsconfig.spec.json b/desktop/angular/projects/safing/portmaster-api/tsconfig.spec.json new file mode 100644 index 00000000..258250d2 --- /dev/null +++ b/desktop/angular/projects/safing/portmaster-api/tsconfig.spec.json @@ -0,0 +1,18 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "../../../out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "testing/**/*.ts" + ], + "include": [ + "testing/**/*.ts", + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/desktop/angular/projects/safing/ui/.eslintrc.json b/desktop/angular/projects/safing/ui/.eslintrc.json new file mode 100644 index 00000000..91e1f496 --- /dev/null +++ b/desktop/angular/projects/safing/ui/.eslintrc.json @@ -0,0 +1,44 @@ +{ + "extends": "../../../.eslintrc.json", + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts" + ], + "parserOptions": { + "project": [ + "projects/safing/ui/tsconfig.lib.json", + "projects/safing/ui/tsconfig.spec.json" + ], + "createDefaultProgram": true + }, + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "sfng", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "sfng", + "style": "kebab-case" + } + ] + } + }, + { + "files": [ + "*.html" + ], + "rules": {} + } + ] +} diff --git a/desktop/angular/projects/safing/ui/README.md b/desktop/angular/projects/safing/ui/README.md new file mode 100644 index 00000000..cf11e371 --- /dev/null +++ b/desktop/angular/projects/safing/ui/README.md @@ -0,0 +1,24 @@ +# Ui + +This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 12.2.0. + +## Code scaffolding + +Run `ng generate component component-name --project ui` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project ui`. +> Note: Don't forget to add `--project ui` or else it will be added to the default project in your `angular.json` file. + +## Build + +Run `ng build ui` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Publishing + +After building your library with `ng build ui`, go to the dist folder `cd dist/ui` and run `npm publish`. + +## Running unit tests + +Run `ng test ui` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/desktop/angular/projects/safing/ui/karma.conf.js b/desktop/angular/projects/safing/ui/karma.conf.js new file mode 100644 index 00000000..8975477b --- /dev/null +++ b/desktop/angular/projects/safing/ui/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, '../../../coverage/safing/ui'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/desktop/angular/projects/safing/ui/ng-package.json b/desktop/angular/projects/safing/ui/ng-package.json new file mode 100644 index 00000000..4a890c44 --- /dev/null +++ b/desktop/angular/projects/safing/ui/ng-package.json @@ -0,0 +1,11 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist-lib/safing/ui", + "lib": { + "entryFile": "src/public-api.ts" + }, + "assets": [ + "theming.scss", + "**/_*.scss" + ] +} diff --git a/desktop/angular/projects/safing/ui/package.json b/desktop/angular/projects/safing/ui/package.json new file mode 100644 index 00000000..52fa541a --- /dev/null +++ b/desktop/angular/projects/safing/ui/package.json @@ -0,0 +1,17 @@ +{ + "name": "@safing/ui", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "~12.2.0", + "@angular/core": "~12.2.0", + "@angular/cdk": "~12.2.0" + }, + "dependencies": { + "tslib": "^2.3.0" + }, + "exports": { + "./theming": { + "sass": "./theming.scss" + } + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/accordion/accordion-group.html b/desktop/angular/projects/safing/ui/src/lib/accordion/accordion-group.html new file mode 100644 index 00000000..6dbc7430 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/accordion/accordion-group.html @@ -0,0 +1 @@ + diff --git a/desktop/angular/projects/safing/ui/src/lib/accordion/accordion-group.ts b/desktop/angular/projects/safing/ui/src/lib/accordion/accordion-group.ts new file mode 100644 index 00000000..3c152842 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/accordion/accordion-group.ts @@ -0,0 +1,116 @@ +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ChangeDetectionStrategy, Component, Input, OnDestroy, TemplateRef } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { SfngAccordionComponent } from './accordion'; + +@Component({ + selector: 'sfng-accordion-group', + templateUrl: './accordion-group.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SfngAccordionGroupComponent implements OnDestroy { + /** @private Currently registered accordion components */ + accordions: SfngAccordionComponent[] = []; + + /** + * A template-ref to render as the header for each accordion-component. + * Receives the accordion data as an $implicit context. + */ + @Input() + set headerTemplate(v: TemplateRef | null) { + this._headerTemplate = v; + + if (!!this.accordions.length) { + this.accordions.forEach(a => { + a.headerTemplate = v; + a.cdr.markForCheck(); + }) + } + } + get headerTemplate() { return this._headerTemplate } + private _headerTemplate: TemplateRef | null = null; + + /** Whether or not one or more components can be expanded. */ + @Input() + set singleMode(v: any) { + this._singleMode = coerceBooleanProperty(v); + } + get singleMode() { return this._singleMode } + private _singleMode = false; + + /** Whether or not the accordion is disabled and does not allow expanding */ + @Input() + set disabled(v: any) { + this._disabled = coerceBooleanProperty(v); + if (this._disabled) { + this.accordions.forEach(a => a.active = false); + } + } + get disabled(): boolean { return this._disabled; } + private _disabled = false; + + /** A list of subscriptions to the activeChange output of the registered accordion-components */ + private subscriptions: Subscription[] = []; + + /** + * Registeres an accordion component to be handled together with this + * accordion group. + * + * @param a The accordion component to register + */ + register(a: SfngAccordionComponent) { + this.accordions.push(a); + + // Tell the accordion-component about the default header-template. + if (!a.headerTemplate) { + a.headerTemplate = this.headerTemplate; + } + + // Subscribe to the activeChange output of the registered + // accordion and call toggle() for each event emitted. + this.subscriptions.push(a.activeChange.subscribe(() => { + if (this.disabled) { + return; + } + + this.toggle(a); + })) + } + + /** + * Unregisters a accordion component + * + * @param a The accordion component to unregister + */ + unregister(a: SfngAccordionComponent) { + const index = this.accordions.indexOf(a); + if (index === -1) return; + + const subscription = this.subscriptions[index]; + + subscription.unsubscribe(); + this.accordions = this.accordions.splice(index, 1); + this.subscriptions = this.subscriptions.splice(index, 1); + } + + ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); + this.subscriptions = []; + this.accordions = []; + } + + /** + * Expand an accordion component and collaps all others if + * single-mode is selected. + * + * @param a The accordion component to toggle. + */ + private toggle(a: SfngAccordionComponent) { + if (!a.active && this._singleMode) { + this.accordions?.forEach(a => a.active = false); + } + + a.active = !a.active; + } + +} diff --git a/desktop/angular/projects/safing/ui/src/lib/accordion/accordion.html b/desktop/angular/projects/safing/ui/src/lib/accordion/accordion.html new file mode 100644 index 00000000..4d47b842 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/accordion/accordion.html @@ -0,0 +1,10 @@ +
+ + +
+ +
+ + + +
diff --git a/desktop/angular/projects/safing/ui/src/lib/accordion/accordion.module.ts b/desktop/angular/projects/safing/ui/src/lib/accordion/accordion.module.ts new file mode 100644 index 00000000..7de494f9 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/accordion/accordion.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { SfngAccordionComponent } from "./accordion"; +import { SfngAccordionGroupComponent } from "./accordion-group"; + +@NgModule({ + imports: [ + CommonModule, + ], + declarations: [ + SfngAccordionGroupComponent, + SfngAccordionComponent, + ], + exports: [ + SfngAccordionGroupComponent, + SfngAccordionComponent, + ] +}) +export class SfngAccordionModule { } diff --git a/desktop/angular/projects/safing/ui/src/lib/accordion/accordion.ts b/desktop/angular/projects/safing/ui/src/lib/accordion/accordion.ts new file mode 100644 index 00000000..1c3f6ec5 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/accordion/accordion.ts @@ -0,0 +1,88 @@ +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostBinding, Input, OnDestroy, OnInit, Optional, Output, TemplateRef, TrackByFunction } from '@angular/core'; +import { fadeInAnimation, fadeOutAnimation } from '../animations'; +import { SfngAccordionGroupComponent } from './accordion-group'; + +@Component({ + selector: 'sfng-accordion', + templateUrl: './accordion.html', + exportAs: 'sfngAccordion', + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + fadeInAnimation, + fadeOutAnimation + ] +}) +export class SfngAccordionComponent implements OnInit, OnDestroy { + /** @deprecated in favor of [data] */ + @Input() + title: string = ''; + + /** A reference to the component provided via the template context */ + component = this; + + /** + * The data the accordion component is used for. This is passed as an $implicit context + * to the header template. + */ + @Input() + data: T | undefined = undefined; + + @Input() + trackBy: TrackByFunction = (_, c) => c + + /** Whether or not the accordion component starts active. */ + @Input() + set active(v: any) { + this._active = coerceBooleanProperty(v); + } + get active() { + return this._active; + } + private _active: boolean = false; + + /** Emits whenever the active value changes. Supports two-way bindings. */ + @Output() + activeChange = new EventEmitter(); + + /** + * The header-template to render for this component. If null, the default template from + * the parent accordion-group will be used. + */ + @Input() + headerTemplate: TemplateRef | null = null; + + @HostBinding('class.active') + /** @private Whether or not the accordion should have the 'active' class */ + get activeClass(): string { + return this.active; + } + + ngOnInit(): void { + // register at our parent group-component (if any). + this.group?.register(this); + } + + ngOnDestroy(): void { + this.group?.unregister(this); + } + + /** + * Toggle the active-state of the accordion-component. + * + * @param event The mouse event. + */ + toggle(event?: Event) { + if (!!this.group && this.group.disabled) { + return; + } + + event?.preventDefault(); + this.activeChange.emit(!this.active); + } + + constructor( + public cdr: ChangeDetectorRef, + @Optional() public group: SfngAccordionGroupComponent, + ) { } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/accordion/index.ts b/desktop/angular/projects/safing/ui/src/lib/accordion/index.ts new file mode 100644 index 00000000..c06e6707 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/accordion/index.ts @@ -0,0 +1,4 @@ +export { SfngAccordionComponent } from './accordion'; +export { SfngAccordionGroupComponent } from './accordion-group'; +export { SfngAccordionModule } from './accordion.module'; + diff --git a/desktop/angular/projects/safing/ui/src/lib/animations/index.ts b/desktop/angular/projects/safing/ui/src/lib/animations/index.ts new file mode 100644 index 00000000..e1613052 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/animations/index.ts @@ -0,0 +1,88 @@ +import { animate, query, stagger, style, transition, trigger } from '@angular/animations'; + +export const fadeInAnimation = trigger( + 'fadeIn', + [ + transition( + ':enter', + [ + style({ opacity: 0, transform: 'translateY(-5px)' }), + animate('120ms cubic-bezier(0, 0, 0.2, 1)', + style({ opacity: 1, transform: 'translateY(0px)' })) + ] + ), + ] +); + +export const fadeOutAnimation = trigger( + 'fadeOut', + [ + transition( + ':leave', + [ + style({ opacity: 1, transform: 'translateY(0px)' }), + animate('120ms cubic-bezier(0, 0, 0.2, 1)', + style({ opacity: 0, transform: 'translateY(-5px)' })) + ] + ), + ] +); + +export const fadeInListAnimation = trigger( + 'fadeInList', + [ + transition(':enter, * => 0, * => -1', []), + transition(':increment', [ + query(':enter', [ + style({ opacity: 0 }), + stagger(5, [ + animate('300ms ease-out', style({ opacity: 1 })), + ]), + ], { optional: true }) + ]), + ] +) + +export const moveInOutAnimation = trigger( + 'moveInOut', + [ + transition( + ':enter', + [ + style({ opacity: 0, transform: 'translateX(100%)' }), + animate('.2s ease-in', + style({ opacity: 1, transform: 'translateX(0%)' })) + ] + ), + transition( + ':leave', + [ + style({ opacity: 1 }), + animate('.2s ease-out', + style({ opacity: 0, transform: 'translateX(100%)' })) + ] + ) + ] +) + +export const moveInOutListAnimation = trigger( + 'moveInOutList', + [ + transition(':enter, * => 0, * => -1', []), + transition(':increment', [ + query(':enter', [ + style({ opacity: 0, transform: 'translateX(100%)' }), + stagger(50, [ + animate('200ms ease-out', style({ opacity: 1, transform: 'translateX(0%)' })), + ]), + ], { optional: true }) + ]), + transition(':decrement', [ + query(':leave', [ + stagger(-50, [ + animate('200ms ease-out', style({ opacity: 0, transform: 'translateX(100%)' })), + ]), + ], { optional: true }) + ]), + ] +) diff --git a/desktop/angular/projects/safing/ui/src/lib/dialog/_confirm.dialog.scss b/desktop/angular/projects/safing/ui/src/lib/dialog/_confirm.dialog.scss new file mode 100644 index 00000000..a0d459a8 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/dialog/_confirm.dialog.scss @@ -0,0 +1,95 @@ +.sfng-confirm-dialog { + display: flex; + flex-direction: column; + align-items: flex-start; + + caption { + @apply text-sm; + opacity: .6; + font-size: .6rem; + } + + h1 { + font-size: 0.85rem; + font-weight: 500; + margin-bottom: 1rem; + } + + .message, + h1 { + flex-shrink: 0; + text-overflow: ellipsis; + word-break: normal; + } + + .message { + font-size: 0.75rem; + flex-grow: 1; + opacity: .6; + max-width: 300px; + } + + .message~input { + margin-top: 0.5rem; + font-size: 95%; + } + + .close-icon { + position: absolute; + top: 1rem; + right: 1rem; + opacity: .7; + cursor: pointer; + + &:hover { + opacity: 1; + } + } + + input[type="text"] { + @apply text-primary; + @apply bg-gray-500 border-gray-400 bg-opacity-75 border-opacity-75; + + &::placeholder { + @apply text-tertiary; + } + } + + .actions { + margin-top: 1rem; + width: 100%; + display: flex; + justify-content: flex-end; + align-items: center; + + button.action-button { + &:not(:last-child) { + margin-right: 0.5rem; + } + + &:not(.outline) { + @apply bg-blue; + } + + &.danger { + @apply bg-red-300; + } + + &.outline { + @apply outline-none; + @apply border; + @apply border-gray-400; + } + } + + &>span { + display: flex; + align-items: center; + + label { + margin-left: .5rem; + user-select: none; + } + } + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/dialog/_dialog.scss b/desktop/angular/projects/safing/ui/src/lib/dialog/_dialog.scss new file mode 100644 index 00000000..22300126 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/dialog/_dialog.scss @@ -0,0 +1,28 @@ +sfng-dialog-container { + .container { + display: block; + box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.75); + @apply p-6; + @apply bg-gray-300; + @apply rounded; + min-width: 20rem; + width: fit-content; + position: relative; + } + + #drag-handle { + display: block; + height: 6px; + background-color: white; + opacity: .4; + border-radius: 3px; + position: absolute; + bottom: calc(0.5rem - 2px); + width: 30%; + left: calc(50% - 15%); + + &:hover { + opacity: .8; + } + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/dialog/confirm.dialog.html b/desktop/angular/projects/safing/ui/src/lib/dialog/confirm.dialog.html new file mode 100644 index 00000000..0bbcf275 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/dialog/confirm.dialog.html @@ -0,0 +1,22 @@ +
+ {{config.caption}} + + + + + +

{{config.header}}

+ + {{ config.message }} + + + +
+ +
+
diff --git a/desktop/angular/projects/safing/ui/src/lib/dialog/confirm.dialog.ts b/desktop/angular/projects/safing/ui/src/lib/dialog/confirm.dialog.ts new file mode 100644 index 00000000..c3c1f888 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/dialog/confirm.dialog.ts @@ -0,0 +1,40 @@ +import { ChangeDetectionStrategy, Component, Inject, InjectionToken } from '@angular/core'; +import { SfngDialogRef, SFNG_DIALOG_REF } from './dialog.ref'; + +export interface ConfirmDialogButton { + text: string; + id: string; + class?: 'danger' | 'outline'; +} + +export interface ConfirmDialogConfig { + buttons?: ConfirmDialogButton[]; + canCancel?: boolean; + header?: string; + message?: string; + caption?: string; + inputType?: 'text' | 'password'; + inputModel?: string; + inputPlaceholder?: string; +} + +export const CONFIRM_DIALOG_CONFIG = new InjectionToken('ConfirmDialogConfig'); + +@Component({ + templateUrl: './confirm.dialog.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SfngConfirmDialogComponent { + constructor( + @Inject(SFNG_DIALOG_REF) private dialogRef: SfngDialogRef, + @Inject(CONFIRM_DIALOG_CONFIG) public config: ConfirmDialogConfig, + ) { + if (config.inputType !== undefined && config.inputModel === undefined) { + config.inputModel = ''; + } + } + + select(action?: string) { + this.dialogRef.close(action || null); + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/dialog/dialog.animations.ts b/desktop/angular/projects/safing/ui/src/lib/dialog/dialog.animations.ts new file mode 100644 index 00000000..14e0fe29 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/dialog/dialog.animations.ts @@ -0,0 +1,19 @@ +import { animate, state, style, transition, trigger } from "@angular/animations"; + +export const dialogAnimation = trigger( + 'dialogContainer', + [ + state('void, exit', style({ opacity: 0, transform: 'scale(0.7)' })), + state('enter', style({ transform: 'none', opacity: 1 })), + transition( + '* => enter', + animate('120ms cubic-bezier(0, 0, 0.2, 1)', + style({ opacity: 1, transform: 'translateY(0px)' })) + ), + transition( + '* => void, * => exit', + animate('120ms cubic-bezier(0, 0, 0.2, 1)', + style({ opacity: 0, transform: 'scale(0.7)' })) + ), + ] +); diff --git a/desktop/angular/projects/safing/ui/src/lib/dialog/dialog.container.ts b/desktop/angular/projects/safing/ui/src/lib/dialog/dialog.container.ts new file mode 100644 index 00000000..d3565f47 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/dialog/dialog.container.ts @@ -0,0 +1,76 @@ +import { AnimationEvent } from '@angular/animations'; +import { CdkDrag } from '@angular/cdk/drag-drop'; +import { CdkPortalOutlet, ComponentPortal, Portal, TemplatePortal } from '@angular/cdk/portal'; +import { ChangeDetectorRef, Component, ComponentRef, EmbeddedViewRef, HostBinding, HostListener, InjectionToken, Input, ViewChild } from '@angular/core'; +import { Subject } from 'rxjs'; +import { dialogAnimation } from './dialog.animations'; + +export const SFNG_DIALOG_PORTAL = new InjectionToken>('SfngDialogPortal'); + +export type SfngDialogState = 'opening' | 'open' | 'closing' | 'closed'; + +@Component({ + selector: 'sfng-dialog-container', + template: ` +
+
+ +
+ `, + animations: [dialogAnimation] +}) +export class SfngDialogContainerComponent { + onStateChange = new Subject(); + + ref: ComponentRef | EmbeddedViewRef | null = null; + + constructor( + private cdr: ChangeDetectorRef, + ) { } + + @HostBinding('@dialogContainer') + state = 'enter'; + + @ViewChild(CdkPortalOutlet, { static: true }) + _portalOutlet: CdkPortalOutlet | null = null; + + @ViewChild(CdkDrag, { static: true }) + drag!: CdkDrag; + + attachComponentPortal(portal: ComponentPortal): ComponentRef { + this.ref = this._portalOutlet!.attachComponentPortal(portal) + return this.ref; + } + + attachTemplatePortal(portal: TemplatePortal): EmbeddedViewRef { + this.ref = this._portalOutlet!.attachTemplatePortal(portal); + return this.ref; + } + + @Input() + dragable: boolean = false; + + @HostListener('@dialogContainer.start', ['$event']) + onAnimationStart({ toState }: AnimationEvent) { + if (toState === 'enter') { + this.onStateChange.next('opening'); + } else if (toState === 'exit') { + this.onStateChange.next('closing'); + } + } + + @HostListener('@dialogContainer.done', ['$event']) + onAnimationEnd({ toState }: AnimationEvent) { + if (toState === 'enter') { + this.onStateChange.next('open'); + } else if (toState === 'exit') { + this.onStateChange.next('closed'); + } + } + + /** Starts the exit animation */ + _startExit() { + this.state = 'exit'; + this.cdr.markForCheck(); + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/dialog/dialog.module.ts b/desktop/angular/projects/safing/ui/src/lib/dialog/dialog.module.ts new file mode 100644 index 00000000..d47195b9 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/dialog/dialog.module.ts @@ -0,0 +1,23 @@ +import { DragDropModule } from "@angular/cdk/drag-drop"; +import { OverlayModule } from "@angular/cdk/overlay"; +import { PortalModule } from "@angular/cdk/portal"; +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { SfngConfirmDialogComponent } from "./confirm.dialog"; +import { SfngDialogContainerComponent } from "./dialog.container"; + +@NgModule({ + imports: [ + CommonModule, + OverlayModule, + PortalModule, + DragDropModule, + FormsModule, + ], + declarations: [ + SfngDialogContainerComponent, + SfngConfirmDialogComponent, + ] +}) +export class SfngDialogModule { } diff --git a/desktop/angular/projects/safing/ui/src/lib/dialog/dialog.ref.ts b/desktop/angular/projects/safing/ui/src/lib/dialog/dialog.ref.ts new file mode 100644 index 00000000..145c60ca --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/dialog/dialog.ref.ts @@ -0,0 +1,62 @@ +import { OverlayRef } from "@angular/cdk/overlay"; +import { InjectionToken } from "@angular/core"; +import { Observable, PartialObserver, Subject } from "rxjs"; +import { filter, take } from "rxjs/operators"; +import { SfngDialogContainerComponent, SfngDialogState } from "./dialog.container"; + +export const SFNG_DIALOG_REF = new InjectionToken>('SfngDialogRef'); + +export class SfngDialogRef { + constructor( + private _overlayRef: OverlayRef, + private container: SfngDialogContainerComponent, + public readonly data: D, + ) { + this.container.onStateChange + .pipe( + filter(state => state === 'closed'), + take(1) + ) + .subscribe(() => { + this._overlayRef.detach(); + this._overlayRef.dispose(); + this.onClose.next(this.value); + this.onClose.complete(); + }); + } + + get onStateChange(): Observable { + return this.container.onStateChange; + } + + + /** + * @returns The overlayref that holds the dialog container. + */ + overlay() { return this._overlayRef } + + /** + * @returns the instance attached to the dialog container + */ + contentRef() { return this.container.ref! } + + /** Value holds the value passed on close() */ + private value: R | null = null; + + /** + * Emits the result of the dialog and closes the overlay. + */ + onClose = new Subject() + + /** onAction only emits if close() is called with action. */ + onAction(action: T, observer: PartialObserver | ((value: T) => void)): this { + (this.onClose.pipe(filter(val => val === action)) as Observable) + .subscribe(observer as any); // typescript does not select the correct type overload here. + return this; + } + + close(result?: R) { + this.value = result || null; + this.container._startExit(); + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/dialog/dialog.service.ts b/desktop/angular/projects/safing/ui/src/lib/dialog/dialog.service.ts new file mode 100644 index 00000000..e7b80ffc --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/dialog/dialog.service.ts @@ -0,0 +1,154 @@ +import { Overlay, OverlayConfig, OverlayPositionBuilder, PositionStrategy } from '@angular/cdk/overlay'; +import { ComponentPortal, ComponentType, TemplatePortal } from '@angular/cdk/portal'; +import { EmbeddedViewRef, Injectable, Injector } from '@angular/core'; +import { filter, take, takeUntil } from 'rxjs/operators'; +import { ConfirmDialogConfig, CONFIRM_DIALOG_CONFIG, SfngConfirmDialogComponent } from './confirm.dialog'; +import { SfngDialogContainerComponent } from './dialog.container'; +import { SfngDialogModule } from './dialog.module'; +import { SfngDialogRef, SFNG_DIALOG_REF } from './dialog.ref'; + +export interface BaseDialogConfig { + /** whether or not the dialog should close on outside-clicks and ESC */ + autoclose?: boolean; + + /** whether or not a backdrop should be visible */ + backdrop?: boolean | 'light'; + + /** whether or not the dialog should be dragable */ + dragable?: boolean; + + /** + * optional position strategy for the overlay. if omitted, the + * overlay will be centered on the screen + */ + positionStrategy?: PositionStrategy; + + /** + * Optional data for the dialog that is available either via the + * SfngDialogRef for ComponentPortals as an $implicit context value + * for TemplatePortals. + * + * Note, for template portals, data is only set as an $implicit context + * value if it is not yet set in the portal! + */ + data?: any; +} + +export interface ComponentPortalConfig { + injector?: Injector; +} + +@Injectable({ providedIn: SfngDialogModule }) +export class SfngDialogService { + + constructor( + private injector: Injector, + private overlay: Overlay, + ) { } + + position(): OverlayPositionBuilder { + return this.overlay.position(); + } + + create(template: TemplatePortal, opts?: BaseDialogConfig): SfngDialogRef>; + create(target: ComponentType, opts?: BaseDialogConfig & ComponentPortalConfig): SfngDialogRef; + create(target: ComponentType | TemplatePortal, opts: BaseDialogConfig & ComponentPortalConfig = {}): SfngDialogRef { + let position: PositionStrategy = opts?.positionStrategy || this.overlay + .position() + .global() + .centerVertically() + .centerHorizontally(); + + let hasBackdrop = true; + let backdropClass = 'dialog-screen-backdrop'; + if (opts.backdrop !== undefined) { + if (opts.backdrop === false) { + hasBackdrop = false; + } else if (opts.backdrop === 'light') { + backdropClass = 'dialog-screen-backdrop-light'; + } + } + + const cfg = new OverlayConfig({ + scrollStrategy: this.overlay.scrollStrategies.noop(), + positionStrategy: position, + hasBackdrop: hasBackdrop, + backdropClass: backdropClass, + }); + const overlayref = this.overlay.create(cfg); + + // create our dialog container and attach it to the + // overlay. + const containerPortal = new ComponentPortal>( + SfngDialogContainerComponent, + undefined, + this.injector, + ) + const containerRef = containerPortal.attach(overlayref); + + if (!!opts.dragable) { + containerRef.instance.dragable = true; + } + + // create the dialog ref + const dialogRef = new SfngDialogRef(overlayref, containerRef.instance, opts.data); + + // prepare the content portal and attach it to the container + let result: any; + if (target instanceof TemplatePortal) { + let r = containerRef.instance.attachTemplatePortal(target) + + if (!!r.context && typeof r.context === 'object' && !('$implicit' in r.context)) { + r.context = { + $implicit: opts.data, + ...r.context, + } + } + + result = r + } else { + const contentPortal = new ComponentPortal(target, null, Injector.create({ + providers: [ + { + provide: SFNG_DIALOG_REF, + useValue: dialogRef, + } + ], + parent: opts?.injector || this.injector, + })); + result = containerRef.instance.attachComponentPortal(contentPortal); + } + // update the container position now that we have some content. + overlayref.updatePosition(); + + if (!!opts?.autoclose) { + overlayref.outsidePointerEvents() + .pipe(take(1)) + .subscribe(() => dialogRef.close()); + overlayref.keydownEvents() + .pipe( + takeUntil(overlayref.detachments()), + filter(event => event.key === 'Escape') + ) + .subscribe(() => { + dialogRef.close(); + }) + } + return dialogRef; + } + + confirm(opts: ConfirmDialogConfig): SfngDialogRef { + return this.create(SfngConfirmDialogComponent, { + autoclose: opts.canCancel, + injector: Injector.create({ + providers: [ + { + provide: CONFIRM_DIALOG_CONFIG, + useValue: opts, + }, + ], + parent: this.injector, + }) + }) + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/dialog/index.ts b/desktop/angular/projects/safing/ui/src/lib/dialog/index.ts new file mode 100644 index 00000000..538cb300 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/dialog/index.ts @@ -0,0 +1,5 @@ +export { ConfirmDialogConfig } from './confirm.dialog'; +export * from './dialog.module'; +export * from './dialog.ref'; +export * from './dialog.service'; + diff --git a/desktop/angular/projects/safing/ui/src/lib/dropdown/dropdown.html b/desktop/angular/projects/safing/ui/src/lib/dropdown/dropdown.html new file mode 100644 index 00000000..33232ea0 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/dropdown/dropdown.html @@ -0,0 +1,27 @@ +
+ +
+ + + +
+ {{ label }} + + + + +
+
+ + +
+ +
+
diff --git a/desktop/angular/projects/safing/ui/src/lib/dropdown/dropdown.module.ts b/desktop/angular/projects/safing/ui/src/lib/dropdown/dropdown.module.ts new file mode 100644 index 00000000..1bfb6846 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/dropdown/dropdown.module.ts @@ -0,0 +1,18 @@ +import { OverlayModule } from "@angular/cdk/overlay"; +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { SfngDropdownComponent } from "./dropdown"; + +@NgModule({ + imports: [ + CommonModule, + OverlayModule, + ], + declarations: [ + SfngDropdownComponent, + ], + exports: [ + SfngDropdownComponent, + ] +}) +export class SfngDropDownModule { } diff --git a/desktop/angular/projects/safing/ui/src/lib/dropdown/dropdown.ts b/desktop/angular/projects/safing/ui/src/lib/dropdown/dropdown.ts new file mode 100644 index 00000000..3b50a8f5 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/dropdown/dropdown.ts @@ -0,0 +1,216 @@ +import { coerceBooleanProperty, coerceCssPixelValue, coerceNumberProperty } from "@angular/cdk/coercion"; +import { CdkOverlayOrigin, ConnectedPosition, ScrollStrategy, ScrollStrategyOptions } from "@angular/cdk/overlay"; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnInit, Output, Renderer2, TemplateRef, ViewChild } from "@angular/core"; +import { fadeInAnimation, fadeOutAnimation } from '../animations'; + +@Component({ + selector: 'sfng-dropdown', + exportAs: 'sfngDropdown', + templateUrl: './dropdown.html', + styles: [ + ` + :host { + display: block; + } + ` + ], + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [fadeInAnimation, fadeOutAnimation], +}) +export class SfngDropdownComponent implements OnInit { + /** The trigger origin used to open the drop-down */ + @ViewChild('trigger', { read: CdkOverlayOrigin }) + trigger: CdkOverlayOrigin | null = null; + + /** + * The button/drop-down label. Only when not using + * {@Link SfngDropdown.externalTrigger} + */ + @Input() + label: string = ''; + + /** The trigger template to use when {@Link SfngDropdown.externalTrigger} */ + @Input() + triggerTemplate: TemplateRef | null = null; + + /** Set to true to provide an external dropdown trigger template using {@Link SfngDropdown.triggerTemplate} */ + @Input() + set externalTrigger(v: any) { + this._externalTrigger = coerceBooleanProperty(v) + } + get externalTrigger() { + return this._externalTrigger; + } + private _externalTrigger = false; + + /** A list of classes to apply to the overlay element */ + @Input() + overlayClass: string = ''; + + /** Whether or not the drop-down is disabled. */ + @Input() + set disabled(v: any) { + this._disabled = coerceBooleanProperty(v) + } + get disabled() { + return this._disabled; + } + private _disabled = false; + + /** The Y-offset of the drop-down overlay */ + @Input() + set offsetY(v: any) { + this._offsetY = coerceNumberProperty(v); + } + get offsetY() { return this._offsetY } + private _offsetY = 4; + + /** The X-offset of the drop-down overlay */ + @Input() + set offsetX(v: any) { + this._offsetX = coerceNumberProperty(v); + } + get offsetX() { return this._offsetX } + private _offsetX = 0; + + /** The scrollStrategy of the drop-down */ + @Input() + scrollStrategy!: ScrollStrategy; + + /** Whether or not the pop-over is currently shown. Do not modify this directly */ + isOpen = false; + + /** The minimum width of the drop-down */ + @Input() + set minWidth(val: any) { + this._minWidth = coerceCssPixelValue(val) + } + get minWidth() { return this._minWidth } + private _minWidth: string | number = 0; + + /** The maximum width of the drop-down */ + @Input() + set maxWidth(val: any) { + this._maxWidth = coerceCssPixelValue(val) + } + get maxWidth() { return this._maxWidth } + private _maxWidth: string | number | null = null; + + /** The minimum height of the drop-down */ + @Input() + set minHeight(val: any) { + this._minHeight = coerceCssPixelValue(val) + } + get minHeight() { return this._minHeight } + private _minHeight: string | number | null = null; + + /** The maximum width of the drop-down */ + @Input() + set maxHeight(val: any) { + this._maxHeight = coerceCssPixelValue(val) + } + get maxHeight() { return this._maxHeight } + private _maxHeight: string | number | null = null; + + /** Emits whenever the drop-down is opened */ + @Output() + opened = new EventEmitter(); + + /** Emits whenever the drop-down is closed. */ + @Output() + closed = new EventEmitter(); + + @Input() + positions: ConnectedPosition[] = [ + { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top', + }, + { + originX: 'end', + originY: 'top', + overlayX: 'end', + overlayY: 'bottom', + }, + { + originX: 'end', + originY: 'bottom', + overlayX: 'start', + overlayY: 'bottom', + }, + ] + + constructor( + public readonly elementRef: ElementRef, + private changeDetectorRef: ChangeDetectorRef, + private renderer: Renderer2, + private scrollOptions: ScrollStrategyOptions, + ) { + } + + ngOnInit() { + this.scrollStrategy = this.scrollStrategy || this.scrollOptions.close(); + } + + onOutsideClick(event: MouseEvent) { + if (!!this.trigger) { + const triggerEl = this.trigger.elementRef.nativeElement; + + let node = event.target; + while (!!node) { + if (node === triggerEl) { + return; + } + node = this.renderer.parentNode(node); + } + } + + this.close(); + } + + onOverlayClosed() { + this.closed.next(); + } + + close() { + if (!this.isOpen) { + return; + } + + this.isOpen = false; + this.changeDetectorRef.markForCheck(); + } + + toggle(t: CdkOverlayOrigin | null = this.trigger) { + if (this.isOpen) { + this.close(); + + return; + } + + this.show(t); + } + + show(t: CdkOverlayOrigin | null = this.trigger) { + if (t === null) { + return; + } + + if (this.isOpen || this._disabled) { + return; + } + + if (!!t) { + this.trigger = t; + const rect = (this.trigger.elementRef.nativeElement as HTMLElement).getBoundingClientRect() + + this.minWidth = rect ? rect.width : this.trigger.elementRef.nativeElement.offsetWidth; + + } + this.isOpen = true; + this.opened.next(); + this.changeDetectorRef.markForCheck(); + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/dropdown/index.ts b/desktop/angular/projects/safing/ui/src/lib/dropdown/index.ts new file mode 100644 index 00000000..ba7a9834 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/dropdown/index.ts @@ -0,0 +1,3 @@ +export * from './dropdown'; +export * from './dropdown.module'; + diff --git a/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/index.ts b/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/index.ts new file mode 100644 index 00000000..8c797446 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/index.ts @@ -0,0 +1,5 @@ +export * from './overlay-stepper'; +export * from './overlay-stepper.module'; +export * from './refs'; +export * from './step'; + diff --git a/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/overlay-stepper-container.html b/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/overlay-stepper-container.html new file mode 100644 index 00000000..5da1fb3e --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/overlay-stepper-container.html @@ -0,0 +1,22 @@ + + + + +
+ +
+ + + + + + +
+ + +
+
diff --git a/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/overlay-stepper-container.ts b/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/overlay-stepper-container.ts new file mode 100644 index 00000000..18492f21 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/overlay-stepper-container.ts @@ -0,0 +1,261 @@ +import { animate, style, transition, trigger } from "@angular/animations"; +import { CdkPortalOutlet, ComponentPortal, ComponentType } from "@angular/cdk/portal"; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentRef, Inject, InjectionToken, Injector, isDevMode, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { Subject } from "rxjs"; +import { SfngDialogRef, SFNG_DIALOG_REF } from "../dialog"; +import { StepperControl, StepRef, STEP_REF } from "./refs"; +import { Step, StepperConfig } from "./step"; +import { StepOutletComponent, STEP_ANIMATION_DIRECTION, STEP_PORTAL } from "./step-outlet"; + +/** + * STEP_CONFIG is used to inject the StepperConfig into the OverlayStepperContainer. + */ +export const STEP_CONFIG = new InjectionToken('StepperConfig'); + +@Component({ + templateUrl: './overlay-stepper-container.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styles: [ + ` + :host { + position: relative; + display: flex; + flex-direction: column; + width: 600px; + } + ` + ], + animations: [ + trigger( + 'moveInOut', + [ + transition( + ':enter', + [ + style({ opacity: 0, transform: 'translateX({{ in }})' }), + animate('.2s cubic-bezier(0.4, 0, 0.2, 1)', + style({ opacity: 1, transform: 'translateX(0%)' })) + ], + { params: { in: '100%' } } // default parameters + ), + transition( + ':leave', + [ + style({ opacity: 1 }), + animate('.2s cubic-bezier(0.4, 0, 0.2, 1)', + style({ opacity: 0, transform: 'translateX({{ out }})' })) + ], + { params: { out: '-100%' } } // default parameters + ) + ] + )] +}) +export class OverlayStepperContainerComponent implements OnInit, OnDestroy, StepperControl { + /** Used to keep cache the stepRef instances. See documentation for {@class StepRef} */ + private stepRefCache = new Map(); + + /** Used to emit when the stepper finished. This is always folled by emitting on onClose$ */ + private onFinish$ = new Subject(); + + /** Emits when the stepper finished - also see {@link OverlayStepperContainerComponent.onClose}*/ + get onFinish() { + return this.onFinish$.asObservable(); + } + + /** + * Emits when the stepper is closed. + * If the stepper if finished then onFinish will emit first + */ + get onClose() { + return this.dialogRef.onClose; + } + + /** The index of the currently displayed step */ + currentStepIndex = -1; + + /** The component instance of the current step */ + currentStep: Step | null = null; + + /** A reference to the portalOutlet used to render our steps */ + @ViewChild(CdkPortalOutlet, { static: true }) + portalOutlet!: CdkPortalOutlet; + + /** Whether or not the user can go back */ + canGoBack = false; + + /** Whether or not the user can abort and close the stepper */ + canAbort = false; + + /** Whether the current step is the last step */ + get isLast() { + return this.currentStepIndex + 1 >= this.config.steps.length; + } + + constructor( + @Inject(STEP_CONFIG) public readonly config: StepperConfig, + @Inject(SFNG_DIALOG_REF) public readonly dialogRef: SfngDialogRef, + private injector: Injector, + private cdr: ChangeDetectorRef + ) { } + + /** + * Moves forward to the next step or closes the stepper + * when moving beyond the last one. + */ + next(): Promise { + if (this.isLast) { + this.onFinish$.next(); + this.close(); + + return Promise.resolve(); + } + + return this.attachStep(this.currentStepIndex + 1, true) + } + + /** + * Moves back to the previous step. This does not take canGoBack + * into account. + */ + goBack(): Promise { + return this.attachStep(this.currentStepIndex - 1, false) + } + + + /** Closes the stepper - this does not run the onFinish hooks of the steps */ + async close(): Promise { + this.dialogRef.close(); + } + + ngOnInit(): void { + this.next(); + } + + ngOnDestroy(): void { + this.onFinish$.complete(); + } + + /** + * Attaches a new step component in the current outlet. It detaches any previous + * step and calls onBeforeBack and onBeforeNext respectively. + * + * @param index The index of the new step to attach. + * @param forward Whether or not the new step is attached by going "forward" or "backward" + * @returns + */ + private async attachStep(index: number, forward = true) { + if (index >= this.config.steps.length) { + if (isDevMode()) { + throw new Error(`Cannot attach step at ${index}: index out of range`) + } + return; + } + + // call onBeforeNext or onBeforeBack of the current step + if (this.currentStep) { + if (forward) { + if (!!this.currentStep.onBeforeNext) { + try { + await this.currentStep.onBeforeNext(); + } catch (err) { + console.error(`Failed to move to next step`, err) + // TODO(ppacher): display error + + return; + } + } + } else { + if (!!this.currentStep.onBeforeBack) { + try { + await this.currentStep.onBeforeBack() + } catch (err) { + console.error(`Step onBeforeBack callback failed`, err) + } + } + } + + // detach the current step component. + this.portalOutlet.detach(); + } + + const stepType = this.config.steps[index]; + const contentPortal = this.createStepContentPortal(stepType, index) + const outletPortal = this.createStepOutletPortal(contentPortal, forward ? 'right' : 'left') + + // attach the new step (which is wrapped in a StepOutletComponent). + const ref = this.portalOutlet.attachComponentPortal(outletPortal); + + // We need to wait for the step to be actually attached in the outlet + // to get access to the actual step component instance. + ref.instance.portalOutlet!.attached + .subscribe((stepRef: ComponentRef) => { + this.currentStep = stepRef.instance; + this.currentStepIndex = index; + + if (typeof this.config.canAbort === 'function') { + this.canAbort = this.config.canAbort(this.currentStepIndex, this.currentStep); + } + + // make sure we trigger a change-detection cycle now + // markForCheck() is not enough here as we need a CD to run + // immediately for the Step.buttonTemplate to be accounted for correctly. + this.cdr.detectChanges(); + }) + } + + /** + * Creates a new component portal for a step and provides access to the {@class StepRef} + * using dependency injection. + * + * @param stepType The component type of the step for which a new portal should be created. + * @param index The index of the current step. Used to create/cache the {@class StepRef} + */ + private createStepContentPortal(stepType: ComponentType, index: number): ComponentPortal { + let stepRef = this.stepRefCache.get(index); + if (stepRef === undefined) { + stepRef = new StepRef(index, this) + this.stepRefCache.set(index, stepRef); + } + + const injector = Injector.create({ + providers: [ + { + provide: STEP_REF, + useValue: stepRef, + } + ], + parent: this.config.injector || this.injector, + }) + + return new ComponentPortal(stepType, undefined, injector); + } + + /** + * Creates a new component portal for a step outlet component that will attach another content + * portal and wrap the attachment in a "move in" animation for a given direction. + * + * @param contentPortal The portal of the actual content that should be attached in the outlet + * @param dir The direction for the animation of the step outlet. + */ + private createStepOutletPortal(contentPortal: ComponentPortal, dir: 'left' | 'right'): ComponentPortal { + const injector = Injector.create({ + providers: [ + { + provide: STEP_PORTAL, + useValue: contentPortal, + }, + { + provide: STEP_ANIMATION_DIRECTION, + useValue: dir, + }, + ], + parent: this.injector, + }) + + return new ComponentPortal( + StepOutletComponent, + undefined, + injector, + ) + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/overlay-stepper.module.ts b/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/overlay-stepper.module.ts new file mode 100644 index 00000000..6bf5fa63 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/overlay-stepper.module.ts @@ -0,0 +1,21 @@ +import { OverlayModule } from "@angular/cdk/overlay"; +import { PortalModule } from "@angular/cdk/portal"; +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { SfngDialogModule } from "../dialog"; +import { OverlayStepperContainerComponent } from "./overlay-stepper-container"; +import { StepOutletComponent } from "./step-outlet"; + +@NgModule({ + imports: [ + CommonModule, + PortalModule, + OverlayModule, + SfngDialogModule, + ], + declarations: [ + OverlayStepperContainerComponent, + StepOutletComponent, + ] +}) +export class OverlayStepperModule { } diff --git a/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/overlay-stepper.ts b/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/overlay-stepper.ts new file mode 100644 index 00000000..4795777a --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/overlay-stepper.ts @@ -0,0 +1,57 @@ +import { ComponentRef, Injectable, Injector } from "@angular/core"; +import { SfngDialogService } from "../dialog"; +import { OverlayStepperContainerComponent, STEP_CONFIG } from "./overlay-stepper-container"; +import { OverlayStepperModule } from "./overlay-stepper.module"; +import { StepperRef } from "./refs"; +import { StepperConfig } from "./step"; + +@Injectable({ providedIn: OverlayStepperModule }) +export class OverlayStepper { + constructor( + private injector: Injector, + private dialog: SfngDialogService, + ) { } + + /** + * Creates a new overlay stepper given it's configuration and returns + * a reference to the stepper that can be used to wait for or control + * the stepper from outside. + * + * @param config The configuration for the overlay stepper. + */ + create(config: StepperConfig): StepperRef { + // create a new injector for our OverlayStepperContainer + // that holds a reference to the StepperConfig. + const injector = this.createInjector(config); + + const dialogRef = this.dialog.create(OverlayStepperContainerComponent, { + injector: injector, + autoclose: false, + backdrop: 'light', + dragable: false, + }) + + const containerComponentRef = dialogRef.contentRef() as ComponentRef; + + return new StepperRef(containerComponentRef.instance); + } + + /** + * Creates a new dependency injector that provides access to the + * stepper configuration using the STEP_CONFIG injection token. + * + * @param config The stepper configuration to provide using DI + * @returns + */ + private createInjector(config: StepperConfig): Injector { + return Injector.create({ + providers: [ + { + provide: STEP_CONFIG, + useValue: config, + }, + ], + parent: this.injector, + }) + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/refs.ts b/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/refs.ts new file mode 100644 index 00000000..c5ce4433 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/refs.ts @@ -0,0 +1,143 @@ +import { InjectionToken } from "@angular/core"; +import { Observable } from "rxjs"; +import { take } from "rxjs/operators"; +import { OverlayStepperContainerComponent } from "./overlay-stepper-container"; + +/** + * STEP_REF is the injection token that is used to provide a reference to the + * Stepper to each step. + */ +export const STEP_REF = new InjectionToken>('StepRef') + +export interface StepperControl { + /** + * Next should move the stepper forward to the next + * step or close the stepper if no more steps are + * available. + * If the stepper is closed this way all onFinish hooks + * registered at {@link StepRef} are executed. + */ + next(): Promise; + + /** + * goBack should move the stepper back to the previous + * step. This is a no-op if there's no previous step to + * display. + */ + goBack(): Promise; + + /** + * close closes the stepper but does not run any onFinish hooks + * of {@link StepRef}. + */ + close(): Promise; +} + +/** + * StepRef is a reference to the overlay stepper and can be used to control, abort + * or otherwise interact with the stepper. + * + * It is made available to individual steps using the STEP_REF injection token. + * Each step in the OverlayStepper receives it's own StepRef instance and will receive + * a reference to the same instance in case the user goes back and re-opens a step + * again. + * + * Steps should therefore store any configuration data that is needed to restore + * the previous view in the StepRef using it's save() and load() methods. + */ +export class StepRef implements StepperControl { + private onFinishHooks: (() => PromiseLike | void)[] = []; + private data: T | null = null; + + constructor( + private currentStepIndex: number, + private stepContainerRef: OverlayStepperContainerComponent, + ) { + this.stepContainerRef.onFinish + .pipe(take(1)) + .subscribe(() => this.runOnFinishHooks) + } + + next(): Promise { + return this.stepContainerRef.next(); + } + + goBack(): Promise { + return this.stepContainerRef.goBack(); + } + + close(): Promise { + return this.stepContainerRef.close(); + } + + /** + * Save saves data of the current step in the stepper session. + * This data is saved in case the user decides to "go back" to + * to a previous step so the old view can be restored. + * + * @param data The data to save in the stepper session. + */ + save(data: T): void { + this.data = data; + } + + /** + * Load returns the data previously stored using save(). The + * StepperRef automatically makes sure the correct data is returned + * for the current step. + */ + load(): T | null { + return this.data; + } + + /** + * registerOnFinish registers fn to be called when the last step + * completes and the stepper is going to finish. + */ + registerOnFinish(fn: () => PromiseLike | void) { + this.onFinishHooks.push(fn); + } + + /** + * Executes all onFinishHooks in the order they have been defined + * and waits for each hook to complete. + */ + private async runOnFinishHooks() { + for (let i = 0; i < this.onFinishHooks.length; i++) { + let res = this.onFinishHooks[i](); + if (typeof res === 'object' && 'then' in res) { + // res is a PromiseLike so wait for it + try { + await res; + } catch (err) { + console.error(`Failed to execute on-finish hook of step ${this.currentStepIndex}: `, err) + } + } + } + } +} + + +export class StepperRef implements StepperControl { + constructor(private stepContainerRef: OverlayStepperContainerComponent) { } + + next(): Promise { + return this.stepContainerRef.next(); + } + + goBack(): Promise { + return this.stepContainerRef.goBack(); + } + + close(): Promise { + return this.stepContainerRef.close(); + } + + get onFinish(): Observable { + return this.stepContainerRef.onFinish; + } + + get onClose(): Observable { + return this.stepContainerRef.onClose; + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/step-outlet.ts b/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/step-outlet.ts new file mode 100644 index 00000000..75bfab61 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/step-outlet.ts @@ -0,0 +1,90 @@ +import { animate, style, transition, trigger } from "@angular/animations"; +import { CdkPortalOutlet, ComponentPortal } from "@angular/cdk/portal"; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentRef, Inject, InjectionToken, ViewChild } from "@angular/core"; +import { Step } from "./step"; + +export const STEP_PORTAL = new InjectionToken>('STEP_PORTAL') +export const STEP_ANIMATION_DIRECTION = new InjectionToken<'left' | 'right'>('STEP_ANIMATION_DIRECTION'); + +/** + * A simple wrapper component around CdkPortalOutlet to add nice + * move animations. + */ +@Component({ + template: ` +
+ +
+ `, + styles: [ + ` + :host{ + display: flex; + flex-direction: column; + overflow: hidden; + } + ` + ], + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + trigger( + 'moveInOut', + [ + transition( + ':enter', + [ + style({ opacity: 0, transform: 'translateX({{ in }})' }), + animate('.2s ease-in', + style({ opacity: 1, transform: 'translateX(0%)' })) + ], + { params: { in: '100%' } } // default parameters + ), + transition( + ':leave', + [ + style({ opacity: 1 }), + animate('.2s ease-out', + style({ opacity: 0, transform: 'translateX({{ out }})' })) + ], + { params: { out: '-100%' } } // default parameters + ) + ] + )] +}) +export class StepOutletComponent implements AfterViewInit { + /** @private - Whether or not the animation should run. */ + _appAnimate = false; + + /** The actual step instance that has been attached. */ + stepInstance: ComponentRef | null = null; + + /** @private - used in animation interpolation for translateX */ + get in() { + return this._animateDirection == 'left' ? '-100%' : '100%' + } + + /** @private - used in animation interpolation for traslateX */ + get out() { + return this._animateDirection == 'left' ? '100%' : '-100%' + } + + /** The portal outlet in our view used to attach the step */ + @ViewChild(CdkPortalOutlet, { static: true }) + portalOutlet!: CdkPortalOutlet; + + constructor( + @Inject(STEP_PORTAL) public portal: ComponentPortal, + @Inject(STEP_ANIMATION_DIRECTION) public _animateDirection: 'left' | 'right', + private cdr: ChangeDetectorRef + ) { } + + ngAfterViewInit(): void { + this.portalOutlet?.attached + .subscribe(ref => { + this.stepInstance = ref as ComponentRef; + + this._appAnimate = true; + this.cdr.detectChanges(); + }) + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/step.ts b/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/step.ts new file mode 100644 index 00000000..1611ff15 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/overlay-stepper/step.ts @@ -0,0 +1,64 @@ +import { Injector, TemplateRef, Type } from "@angular/core"; +import { Observable } from "rxjs"; + +export interface Step { + /** + * validChange should emit true or false when the current step + * is valid and the "next" button should be visible. + */ + validChange: Observable; + + /** + * onBeforeBack, if it exists, is called when the user + * clicks the "Go Back" button but before the current step + * is unloaded. + * + * The OverlayStepper will wait for the callback to resolve or + * reject but will not abort going back! + */ + onBeforeBack?: () => Promise; + + /** + * onBeforeNext, if it exists, is called when the user + * clicks the "Next" button but before the current step + * is unloaded. + * + * The OverlayStepper willw ait for the callback to resolve + * or reject. If it rejects the current step will not be unloaded + * and the rejected error will be displayed to the user. + */ + onBeforeNext?: () => Promise; + + /** + * nextButtonLabel can overwrite the label for the "Next" button. + */ + nextButtonLabel?: string; + + /** + * buttonTemplate may hold a tempalte ref that is rendered instead + * of the default button row with a "Go Back" and a "Next" button. + * Note that if set, the step component must make sure to handle + * navigation itself. See {@class StepRef} for more information on how + * to control the stepper. + */ + buttonTemplate?: TemplateRef; +} + +export interface StepperConfig { + /** + * canAbort can be set to a function that is called + * for each step to determine if the stepper is abortable. + */ + canAbort?: (idx: number, step: Step) => boolean; + + /** steps holds the list of steps to execute */ + steps: Array> + + /** + * injector, if set, defines the parent injector used to + * create dedicated instances of the step types. + */ + injector?: Injector; +} + + diff --git a/desktop/angular/projects/safing/ui/src/lib/pagination/_pagination.scss b/desktop/angular/projects/safing/ui/src/lib/pagination/_pagination.scss new file mode 100644 index 00000000..46b8bdaf --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/pagination/_pagination.scss @@ -0,0 +1,22 @@ +sfng-pagination { + .pagination { + @apply my-2 w-full flex justify-between; + + button { + @apply text-xxs px-2 flex items-center justify-start; + + &.page { + @apply bg-cards-secondary; + @apply opacity-50; + + &:hover { + @apply opacity-100; + } + } + + &.active-page { + @apply text-blue font-medium opacity-100; + } + } + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/pagination/dynamic-items-paginator.ts b/desktop/angular/projects/safing/ui/src/lib/pagination/dynamic-items-paginator.ts new file mode 100644 index 00000000..b3f8a833 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/pagination/dynamic-items-paginator.ts @@ -0,0 +1,64 @@ + +import { BehaviorSubject, Observable, Subscription } from "rxjs"; +import { Pagination, clipPage } from "./pagination"; + +export interface Datasource { + // view should emit all items in the given page using the specified page number. + view(page: number, pageSize: number): Observable; +} + +export class DynamicItemsPaginator implements Pagination { + private _total = 0; + private _pageNumber$ = new BehaviorSubject(1); + private _pageItems$ = new BehaviorSubject([]); + private _pageLoading$ = new BehaviorSubject(false); + private _pageSubscription = Subscription.EMPTY; + + /** Returns the number of total pages. */ + get total() { return this._total; } + + /** Emits the current page number */ + get pageNumber$() { return this._pageNumber$.asObservable() } + + /** Emits all items of the current page */ + get pageItems$() { return this._pageItems$.asObservable() } + + /** Emits whether or not we're loading the next page */ + get pageLoading$() { return this._pageLoading$.asObservable() } + + constructor( + private source: Datasource, + public readonly pageSize = 25, + ) { } + + reset(newTotal: number) { + this._total = Math.ceil(newTotal / this.pageSize); + this.openPage(1); + } + + /** Clear resets the current total and emits an empty item set. */ + clear() { + this._total = 0; + this._pageItems$.next([]); + this._pageNumber$.next(1); + this._pageSubscription.unsubscribe(); + } + + openPage(pageNumber: number): void { + pageNumber = clipPage(pageNumber, this.total); + this._pageLoading$.next(true); + + this._pageSubscription.unsubscribe() + this._pageSubscription = this.source.view(pageNumber, this.pageSize) + .subscribe({ + next: results => { + this._pageLoading$.next(false); + this._pageItems$.next(results); + this._pageNumber$.next(pageNumber); + } + }); + } + + nextPage(): void { this.openPage(this._pageNumber$.getValue() + 1) } + prevPage(): void { this.openPage(this._pageNumber$.getValue() - 1) } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/pagination/index.ts b/desktop/angular/projects/safing/ui/src/lib/pagination/index.ts new file mode 100644 index 00000000..fb2f898c --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/pagination/index.ts @@ -0,0 +1,5 @@ +export * from './dynamic-items-paginator'; +export * from './pagination'; +export * from './pagination.module'; +export * from './snapshot-paginator'; + diff --git a/desktop/angular/projects/safing/ui/src/lib/pagination/pagination.html b/desktop/angular/projects/safing/ui/src/lib/pagination/pagination.html new file mode 100644 index 00000000..dec63df8 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/pagination/pagination.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/desktop/angular/projects/safing/ui/src/lib/pagination/pagination.module.ts b/desktop/angular/projects/safing/ui/src/lib/pagination/pagination.module.ts new file mode 100644 index 00000000..508454ca --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/pagination/pagination.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { SfngPaginationContentDirective } from "."; +import { SfngPaginationWrapperComponent } from "./pagination"; + +@NgModule({ + imports: [ + CommonModule, + ], + declarations: [ + SfngPaginationContentDirective, + SfngPaginationWrapperComponent, + ], + exports: [ + SfngPaginationContentDirective, + SfngPaginationWrapperComponent, + ], +}) +export class SfngPaginationModule { } diff --git a/desktop/angular/projects/safing/ui/src/lib/pagination/pagination.ts b/desktop/angular/projects/safing/ui/src/lib/pagination/pagination.ts new file mode 100644 index 00000000..f3241cc9 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/pagination/pagination.ts @@ -0,0 +1,132 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, Directive, EventEmitter, Input, OnChanges, OnDestroy, Output, SimpleChanges, TemplateRef } from "@angular/core"; +import { Observable, Subscription } from "rxjs"; + +export interface Pagination { + /** + * Total should return the total number of pages + */ + total: number; + + /** + * pageNumber$ should emit the currently displayed page + */ + pageNumber$: Observable; + + /** + * pageItems$ should emit all items of the current page + */ + pageItems$: Observable; + + /** + * nextPage should progress to the next page. If there are no more + * pages than nextPage() should be a no-op. + */ + nextPage(): void; + + /** + * prevPage should move back the the previous page. If there is no + * previous page, prevPage should be a no-op. + */ + prevPage(): void; + + /** + * openPage opens the page @pageNumber. If pageNumber is greater than + * the total amount of pages it is clipped to the lastPage. If it is + * less than 1, it is clipped to 1. + */ + openPage(pageNumber: number): void +} + + + +@Directive({ + selector: '[sfngPageContent]' +}) +export class SfngPaginationContentDirective { + constructor(public readonly templateRef: TemplateRef) { } +} + +export interface PageChangeEvent { + totalPages: number; + currentPage: number; +} + +@Component({ + selector: 'sfng-pagination', + templateUrl: './pagination.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SfngPaginationWrapperComponent implements OnChanges, OnDestroy { + private _sub: Subscription = Subscription.EMPTY; + + @Input() + source: Pagination | null = null; + + @Output() + pageChange = new EventEmitter(); + + @ContentChild(SfngPaginationContentDirective) + content: SfngPaginationContentDirective | null = null; + + currentPageIdx: number = 0; + pageNumbers: number[] = []; + + ngOnChanges(changes: SimpleChanges) { + if ('source' in changes) { + this.subscribeToSource(changes.source.currentValue); + } + } + + ngOnDestroy() { + this._sub.unsubscribe(); + } + + private subscribeToSource(source: Pagination) { + // Unsubscribe from the previous pagination, if any + this._sub.unsubscribe(); + + this._sub = new Subscription(); + + this._sub.add( + source.pageNumber$ + .subscribe(current => { + this.currentPageIdx = current; + this.pageNumbers = generatePageNumbers(current - 1, source.total); + this.cdr.markForCheck(); + + this.pageChange.next({ + totalPages: source.total, + currentPage: current, + }) + }) + ) + } + + constructor(private cdr: ChangeDetectorRef) { } +} + +/** + * Generates an array of page numbers that should be displayed in paginations. + * + * @param current The current page number + * @param countPages The total number of pages + * @returns An array of page numbers to display + */ +export function generatePageNumbers(current: number, countPages: number): number[] { + let delta = 2; + let leftRange = current - delta; + let rightRange = current + delta + 1; + + return Array.from({ length: countPages }, (v, k) => k + 1) + .filter(i => i === 1 || i === countPages || (i >= leftRange && i < rightRange)); +} + +export function clipPage(pageNumber: number, total: number): number { + if (pageNumber < 1) { + return 1; + } + if (pageNumber > total) { + return total; + } + return pageNumber; +} diff --git a/desktop/angular/projects/safing/ui/src/lib/pagination/snapshot-paginator.ts b/desktop/angular/projects/safing/ui/src/lib/pagination/snapshot-paginator.ts new file mode 100644 index 00000000..7f014254 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/pagination/snapshot-paginator.ts @@ -0,0 +1,64 @@ +import { BehaviorSubject, Observable } from "rxjs"; +import { debounceTime, map } from "rxjs/operators"; +import { clipPage, Pagination } from "./pagination"; + +export class SnapshotPaginator implements Pagination { + private _itemSnapshot: T[] = []; + private _activePageItems = new BehaviorSubject([]); + private _totalPages = 1; + private _updatePending = false; + + constructor( + public items$: Observable, + public readonly pageSize: number, + ) { + items$ + .pipe(debounceTime(100)) + .subscribe(data => { + this._itemSnapshot = data; + this.openPage(this._currentPage.getValue()); + }); + + this._currentPage + .subscribe(page => { + this._updatePending = false; + const start = this.pageSize * (page - 1); + const end = this.pageSize * page; + this._totalPages = Math.ceil(this._itemSnapshot.length / this.pageSize) || 1; + this._activePageItems.next(this._itemSnapshot.slice(start, end)); + }) + } + + private _currentPage = new BehaviorSubject(0); + + get updatePending() { + return this._updatePending; + } + get pageNumber$(): Observable { + return this._activePageItems.pipe(map(() => this._currentPage.getValue())); + } + get pageNumber(): number { + return this._currentPage.getValue(); + } + get total(): number { + return this._totalPages + } + get pageItems$(): Observable { + return this._activePageItems.asObservable(); + } + get pageItems(): T[] { + return this._activePageItems.getValue(); + } + get snapshot(): T[] { return this._itemSnapshot }; + + reload(): void { this.openPage(this._currentPage.getValue()) } + + nextPage(): void { this.openPage(this._currentPage.getValue() + 1) } + + prevPage(): void { this.openPage(this._currentPage.getValue() - 1) } + + openPage(pageNumber: number): void { + pageNumber = clipPage(pageNumber, this.total); + this._currentPage.next(pageNumber); + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/select/_select.scss b/desktop/angular/projects/safing/ui/src/lib/select/_select.scss new file mode 100644 index 00000000..0d8cb345 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/select/_select.scss @@ -0,0 +1,73 @@ +.sfng-select { + @apply cursor-pointer relative p-0 flex whitespace-nowrap w-full items-center outline-none self-center overflow-hidden; + @apply hover:bg-gray-400; + @apply bg-gray-300 border border-gray-300 transition ease-in-out duration-200; + + &.disabled { + @apply cursor-not-allowed opacity-75 hover:bg-gray-400; + } + + min-width: 6rem; + max-width: 12rem; + + &.active { + @apply bg-gray-400; + + div.arrow svg { + @apply transform -rotate-90; + } + } + + & > span { + @apply flex-grow text-ellipsis inline-block overflow-hidden; + @apply px-2; + } + + div.arrow { + @apply flex flex-row items-center justify-center bg-gray-200 rounded-r-sm; + @apply w-5 h-7; + + svg { + @apply w-4 m-0 p-0 rotate-90 transform transition ease-in-out duration-100; + + g { + @apply text-white; + stroke: currentColor; + } + } + } +} + +.sfng-select-dropdown { + ul { + max-height: 12rem; + @apply relative py-1 overflow-auto; + + li { + @apply py-2; + @apply flex flex-row items-center justify-start gap-1 transition duration-200 ease-in-out cursor-pointer hover:bg-gray-300; + } + + li:not(.disabled) { + @apply hover:bg-gray-300; + } + + li.disabled { + @apply cursor-not-allowed; + } + } +} + +.sfng-select-dropdown.sfng-select-inline { + ul { + max-height: unset; + } +} + +sfng-select-item { + @apply text-xxs w-full font-medium gap-3 text-primary flex flex-row items-center justify-start; + + &.disabled { + @apply opacity-75 cursor-not-allowed; + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/select/index.ts b/desktop/angular/projects/safing/ui/src/lib/select/index.ts new file mode 100644 index 00000000..1342a276 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/select/index.ts @@ -0,0 +1,4 @@ +export * from './item'; +export * from './select'; +export * from './select.module'; + diff --git a/desktop/angular/projects/safing/ui/src/lib/select/item.ts b/desktop/angular/projects/safing/ui/src/lib/select/item.ts new file mode 100644 index 00000000..b2eb5696 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/select/item.ts @@ -0,0 +1,64 @@ +import { ListKeyManagerOption } from '@angular/cdk/a11y'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Component, Directive, HostBinding, Input, Optional, TemplateRef } from '@angular/core'; + +export interface SelectOption extends ListKeyManagerOption { + value: any; + selected: boolean; + + data?: T; + label?: string; + description?: string; + templateRef?: TemplateRef; + disabled?: boolean; +} + +@Component({ + selector: 'sfng-select-item', + template: ``, +}) +export class SfngSelectItemComponent implements ListKeyManagerOption { + @HostBinding('class.disabled') + get disabled() { + return this.sfngSelectValue?.disabled || false; + } + + getLabel() { + return this.sfngSelectValue?.label || ''; + } + + constructor(@Optional() private sfngSelectValue: SfngSelectValueDirective) { } +} + +@Directive({ + selector: '[sfngSelectValue]', +}) +export class SfngSelectValueDirective implements SelectOption { + @Input('sfngSelectValue') + value: any; + + @Input('sfngSelectValueLabel') + label?: string; + + @Input('sfngSelectValueData') + data?: T; + + @Input('sfngSelectValueDescription') + description = ''; + + @Input('sfngSelectValueDisabled') + set disabled(v: any) { + this._disabled = coerceBooleanProperty(v) + } + get disabled() { return this._disabled } + private _disabled = false; + + getLabel() { + return this.label || ('' + this.value); + } + + /** Whether or not the item is currently selected */ + selected = false; + + constructor(public templateRef: TemplateRef) { } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/select/select.html b/desktop/angular/projects/safing/ui/src/lib/select/select.html new file mode 100644 index 00000000..bccf19af --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/select/select.html @@ -0,0 +1,88 @@ + + + + + + + +
    +
  • + + + + + + + +
  • + + +
  • + + + Add {{ searchText }} + + +
  • +
+
+ + + + + + + +
+ +
+
+ + + + {{ data.label || data.value }} + diff --git a/desktop/angular/projects/safing/ui/src/lib/select/select.module.ts b/desktop/angular/projects/safing/ui/src/lib/select/select.module.ts new file mode 100644 index 00000000..d33fce4d --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/select/select.module.ts @@ -0,0 +1,31 @@ +import { CdkScrollableModule } from "@angular/cdk/scrolling"; +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { SfngDropDownModule } from "../dropdown"; +import { SfngTooltipModule } from "../tooltip"; +import { SfngSelectItemComponent, SfngSelectValueDirective } from "./item"; +import { SfngSelectComponent, SfngSelectRenderedItemDirective } from "./select"; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + SfngDropDownModule, + SfngTooltipModule, + CdkScrollableModule + ], + declarations: [ + SfngSelectComponent, + SfngSelectValueDirective, + SfngSelectItemComponent, + SfngSelectRenderedItemDirective + ], + exports: [ + SfngSelectComponent, + SfngSelectValueDirective, + SfngSelectItemComponent, + ] +}) +export class SfngSelectModule { } diff --git a/desktop/angular/projects/safing/ui/src/lib/select/select.ts b/desktop/angular/projects/safing/ui/src/lib/select/select.ts new file mode 100644 index 00000000..9375f21f --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/select/select.ts @@ -0,0 +1,495 @@ +import { ListKeyManager, ListKeyManagerOption } from '@angular/cdk/a11y'; +import { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, DestroyRef, Directive, ElementRef, EventEmitter, HostBinding, HostListener, Input, OnDestroy, Output, QueryList, TemplateRef, ViewChild, ViewChildren, forwardRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { BehaviorSubject, combineLatest } from 'rxjs'; +import { startWith } from 'rxjs/operators'; +import { SfngDropdownComponent } from '../dropdown'; +import { SelectOption, SfngSelectValueDirective } from './item'; + + +export type SelectModes = 'single' | 'multi'; + +type ModeInput = { + mode: SelectModes; +} + +type SelectValue = S['mode'] extends 'single' ? T : T[]; + +export type SortByFunc = (a: SelectOption, b: SelectOption) => number; + +export type SelectDisplayMode = 'dropdown' | 'inline'; + +@Directive({ + selector: '[sfngSelectRenderedListItem]' +}) +export class SfngSelectRenderedItemDirective implements ListKeyManagerOption { + @Input('sfngSelectRenderedListItem') + option: SelectOption | null = null; + + getLabel() { + return this.option?.label || ''; + } + + get disabled() { + return this.option?.disabled || false; + } + + @HostBinding('class.bg-gray-300') + set focused(v: boolean) { + this._focused = v; + } + get focused() { return this._focused } + private _focused = false; + + constructor(public readonly elementRef: ElementRef) { } +} + +@Component({ + selector: 'sfng-select', + templateUrl: './select.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SfngSelectComponent), + multi: true, + }, + ] +}) +export class SfngSelectComponent implements AfterViewInit, ControlValueAccessor, OnDestroy { + /** emits the search text entered by the user */ + private search$ = new BehaviorSubject(''); + + /** emits and completes when the component is destroyed. */ + private destroyRef = inject(DestroyRef); + + /** the key manager used for keyboard support */ + private keyManager!: ListKeyManager; + + @ViewChild(SfngDropdownComponent, { static: false }) + dropdown: SfngDropdownComponent | null = null; + + /** A reference to the cdk-scrollable directive that's placed on the item list */ + @ViewChild('scrollable', { read: ElementRef }) + scrollableList?: ElementRef; + + @ContentChildren(SfngSelectValueDirective) + userProvidedItems!: QueryList; + + @ViewChildren('renderedItem', { read: SfngSelectRenderedItemDirective }) + renderedItems!: QueryList; + + /** A list of all items available in the select box including dynamic ones. */ + allItems: SelectOption[] = [] + + /** The acutally rendered list of items after applying search and item threshold */ + items: SelectOption[] = []; + + @Input() + @HostBinding('attr.tabindex') + readonly tabindex = 0; + + @HostBinding('attr.role') + readonly role = 'listbox'; + + value?: SelectValue; + + /** A list of currently selected items */ + currentItems: SelectOption[] = []; + + /** The current search text. Used by ngModel */ + searchText = ''; + + /** Whether or not the select operates in "single" or "multi" mode */ + @Input() + mode: SelectModes = 'single'; + + @Input() + displayMode: SelectDisplayMode = 'dropdown'; + + /** The placehodler to show when nothing is selected */ + @Input() + placeholder = 'Select' + + /** The type of item to show in multi mode when more than one value is selected */ + @Input() + itemName = ''; + + /** The maximum number of items to render. */ + @Input() + set itemLimit(v: any) { + this._maxItemLimit = coerceNumberProperty(v) + } + get itemLimit(): number { return this._maxItemLimit } + private _maxItemLimit = Infinity; + + /** The placeholder text for the search bar */ + @Input() + searchPlaceholder = ''; + + /** Whether or not the search bar is visible */ + @Input() + set allowSearch(v: any) { + this._allowSearch = coerceBooleanProperty(v); + } + get allowSearch(): boolean { + return this._allowSearch; + } + private _allowSearch = false; + + /** The minimum number of items required for the search bar to be visible */ + @Input() + set searchItemThreshold(v: any) { + this._searchItemThreshold = coerceNumberProperty(v); + } + get searchItemThreshold(): number { + return this._searchItemThreshold; + } + private _searchItemThreshold = 0; + + /** + * Whether or not the select should be disabled when not options + * are available. + */ + @Input() + set disableWhenEmpty(v: any) { + this._disableWhenEmpty = coerceBooleanProperty(v); + } + get disableWhenEmpty() { + return this._disableWhenEmpty; + } + private _disableWhenEmpty = false; + + /** Whether or not the select component will add options for dynamic values as well. */ + @Input() + set dynamicValues(v: any) { + this._dynamicValues = coerceBooleanProperty(v); + } + get dynamicValues() { + return this._dynamicValues + } + private _dynamicValues = false; + + /** An optional template to use for dynamic values. */ + @Input() + dynamicValueTemplate?: TemplateRef; + + /** The minimum-width of the drop-down. See {@link SfngDropdownComponent.minWidth} */ + @Input() + minWidth: any; + + /** The minimum-width of the drop-down. See {@link SfngDropdownComponent.minHeight} */ + @Input() + minHeight: any; + + /** Whether or not selected items should be sorted to the top */ + @Input() + set sortValues(v: any) { + this._sortValues = coerceBooleanProperty(v); + } + get sortValues() { + if (this._sortValues === null) { + return this.mode === 'multi'; + } + return this._sortValues; + } + private _sortValues: boolean | null = null; + + /** The sort function to use. Defaults to sort by label/value */ + @Input() + sortBy: SortByFunc = (a: SelectOption, b: SelectOption) => { + if ((a.label || a.value) < (b.label || b.value)) { + return 1; + } + if ((a.label || a.value) > (b.label || b.value)) { + return -1; + } + + return 0; + } + + @Input() + set disabled(v: any) { + const disabled = coerceBooleanProperty(v); + this.setDisabledState(disabled); + } + get disabled() { + return this._disabled; + } + private _disabled: boolean = false; + + @HostListener('keydown.enter', ['$event']) + @HostListener('keydown.space', ['$event']) + onEnter(event: Event) { + if (!this.dropdown?.isOpen) { + this.dropdown?.toggle() + + event.preventDefault(); + event.stopPropagation(); + + return; + } + + if (this.keyManager.activeItem !== null && !!this.keyManager.activeItem?.option) { + this.selectItem(this.keyManager.activeItem.option) + + event.preventDefault(); + event.stopPropagation(); + + return; + } + } + + @HostListener('keydown', ['$event']) + onKeyDown(event: KeyboardEvent) { + this.keyManager.onKeydown(event); + } + + @Output() + closed = new EventEmitter(); + + @Output() + opened = new EventEmitter(); + + trackItem(_: number, item: SelectOption) { + return item.value; + } + + setDisabledState(disabled: boolean) { + this._disabled = disabled; + this.cdr.markForCheck(); + } + + constructor(private cdr: ChangeDetectorRef) { } + + ngAfterViewInit(): void { + this.keyManager = new ListKeyManager(this.renderedItems) + .withVerticalOrientation() + .withHomeAndEnd() + .withWrap() + .withTypeAhead(); + + this.keyManager.change + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(itemIdx => { + this.renderedItems.forEach(item => { + item.focused = false; + }) + + this.keyManager.activeItem!.focused = true; + + // the item might be out-of-view so make sure + // we scroll enough to have it inside the view + const scrollable = this.scrollableList?.nativeElement; + if (!!scrollable) { + const active = this.keyManager.activeItem!.elementRef.nativeElement; + const activeHeight = active.getBoundingClientRect().height; + const bottom = scrollable.scrollTop + scrollable.getBoundingClientRect().height; + const top = scrollable.scrollTop; + + let scrollTo = -1; + if (active.offsetTop >= bottom) { + scrollTo = top + active.offsetTop - bottom + activeHeight; + } else if (active.offsetTop < top) { + scrollTo = active.offsetTop; + } + + if (scrollTo > -1) { + scrollable.scrollTo({ + behavior: 'smooth', + top: scrollTo, + }) + } + } + + this.cdr.markForCheck(); + }) + + + combineLatest([ + this.userProvidedItems!.changes + .pipe(startWith(undefined)), + this.search$ + ]) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe( + ([_, search]) => { + this.updateItems(); + + search = (search || '').toLocaleLowerCase() + let items: SelectOption[] = []; + if (search === '') { + items = this.allItems!; + } else { + items = this.allItems!.filter(item => { + // we always count selected items as a "match" in search mode. + // this is to ensure the user always see all selected items. + if (item.selected) { + return true; + } + + if (!!item.value && typeof item.value === 'string') { + if (item.value.toLocaleLowerCase().includes(search)) { + return true; + } + } + + if (!!item.label) { + if (item.label.toLocaleLowerCase().includes(search)) { + return true + } + } + return false; + }) + } + + this.items = items.slice(0, this._maxItemLimit); + this.keyManager.setActiveItem(0); + + this.cdr.detectChanges(); + } + ); + } + + ngOnDestroy(): void { + this.search$.complete(); + } + + @HostListener('blur') + onBlur(): void { + this.onTouch(); + } + + /** @private - called when the internal dropdown opens */ + onDropdownOpen() { + // emit the open event on this component as well + this.opened.next(); + + // reset the search. We do that when opened instead of closed + // to avoid flickering when the component height increases + // during the "close" animation + this.onSearch(''); + } + + /** @private - called when the internal dropdown closes */ + onDropdownClose() { + this.closed.next(); + } + + onSearch(text: string) { + this.searchText = text; + this.search$.next(text); + } + + selectItem(item: SelectOption) { + if (item.disabled) { + return; + } + + const isSelected = this.currentItems.findIndex(selected => item.value === selected.value); + if (isSelected === -1) { + item.selected = true; + + if (this.mode === 'single') { + this.currentItems.forEach(i => i.selected = false); + this.currentItems = [item]; + this.value = item.value; + } else { + this.currentItems.push(item); + // TODO(ppacher): somehow typescript does not correctly pick up + // the type of this.value here although it can be infered from the + // mode === 'single' check above. + this.value = [ + ...(this.value || []) as any, + item.value, + ] as any + } + } else if (this.mode !== 'single') { // "unselecting" a value is not allowed in single mode + this.currentItems.splice(isSelected, 1) + item.selected = false; + // same note about typescript as above. + this.value = (this.value as T[]).filter(val => val !== item.value) as any; + } + + // only close the drop down in single mode. In multi-mode + // we keep it open as the user might want to select an additional + // item as well. + if (this.mode === 'single') { + this.dropdown?.close(); + } + this.onChange(this.value!); + } + + private updateItems() { + let values: T[] = []; + if (this.mode === 'single') { + values = [this.value as T]; + } else { + values = (this.value as T[]) || []; + } + + this.currentItems = []; + this.allItems = []; + + // mark all user-selected items as "deselected" first + this.userProvidedItems?.forEach(item => { + item.selected = false; + this.allItems.push(item); + }); + + for (let i = 0; i < values.length; i++) { + const val = values[i]; + let option: SelectOption | undefined = this.userProvidedItems?.find(item => item.value === val); + if (!option) { + if (!this._dynamicValues) { + continue + } + + option = { + selected: true, + value: val, + label: `${val}`, + } + this.allItems.push(option); + } else { + option.selected = true + } + + this.currentItems.push(option); + } + + if (this.sortValues) { + this.allItems.sort((a, b) => { + if (b.selected && !a.selected) { + return 1; + } + + if (a.selected && !b.selected) { + return -1; + } + + return this.sortBy(a, b) + }) + } + } + + writeValue(value: SelectValue): void { + this.value = value; + + this.updateItems(); + + this.cdr.markForCheck(); + } + + onChange = (value: SelectValue): void => { } + registerOnChange(fn: (value: SelectValue) => void): void { + this.onChange = fn; + } + + onTouch = (): void => { } + registerOnTouched(fn: () => void): void { + this.onTouch = fn; + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/tabs/_tab-group.scss b/desktop/angular/projects/safing/ui/src/lib/tabs/_tab-group.scss new file mode 100644 index 00000000..4d63b670 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tabs/_tab-group.scss @@ -0,0 +1,3 @@ +sfng-tab-group { + @apply flex flex-col overflow-hidden; +} diff --git a/desktop/angular/projects/safing/ui/src/lib/tabs/index.ts b/desktop/angular/projects/safing/ui/src/lib/tabs/index.ts new file mode 100644 index 00000000..4fd3296a --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tabs/index.ts @@ -0,0 +1,4 @@ +export { SfngTabComponent, SfngTabContentDirective } from './tab'; +export { SfngTabContentScrollEvent, SfngTabGroupComponent } from './tab-group'; +export { SfngTabModule as TabModule } from './tabs.module'; + diff --git a/desktop/angular/projects/safing/ui/src/lib/tabs/tab-group.html b/desktop/angular/projects/safing/ui/src/lib/tabs/tab-group.html new file mode 100644 index 00000000..f78ff738 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tabs/tab-group.html @@ -0,0 +1,24 @@ +
+ + +
+ + {{ tab.title }} + + + + + +
+ + +
+
+ + diff --git a/desktop/angular/projects/safing/ui/src/lib/tabs/tab-group.ts b/desktop/angular/projects/safing/ui/src/lib/tabs/tab-group.ts new file mode 100644 index 00000000..e4a65f6e --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tabs/tab-group.ts @@ -0,0 +1,352 @@ +import { ListKeyManager } from "@angular/cdk/a11y"; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { CdkPortalOutlet, ComponentPortal } from "@angular/cdk/portal"; +import { AfterContentInit, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentRef, ContentChildren, DestroyRef, ElementRef, EventEmitter, Injector, Input, OnInit, Output, QueryList, ViewChild, ViewChildren, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute, Router } from "@angular/router"; +import { Observable, Subject } from "rxjs"; +import { distinctUntilChanged, map, startWith } from "rxjs/operators"; +import { SfngTabComponent, TAB_ANIMATION_DIRECTION, TAB_PORTAL, TAB_SCROLL_HANDLER, TabOutletComponent } from "./tab"; + +export interface SfngTabContentScrollEvent { + event?: Event; + scrollTop: number; + previousScrollTop: number; +} + +/** + * Tab group component for rendering a tab-style navigation with support for + * keyboard navigation and type-ahead. Tab content are lazy loaded using a + * structural directive. + * The tab group component also supports adding the current active tab index + * to the active route so it is possible to navigate through tabs using back/forward + * keys (browser history) as well. + * + * Example: + * + * + * + *
+ * Some content + *
+ *
+ * + * + *
+ * Some different content + *
+ *
+ * + *
+ */ +@Component({ + selector: 'sfng-tab-group', + templateUrl: './tab-group.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SfngTabGroupComponent implements AfterContentInit, AfterViewInit, OnInit { + @ContentChildren(SfngTabComponent) + tabs: QueryList | null = null; + + /** References to all tab header elements */ + @ViewChildren('tabHeader', { read: ElementRef }) + tabHeaders: QueryList> | null = null; + + /** Reference to the active tab bar element */ + @ViewChild('activeTabBar', { read: ElementRef, static: false }) + activeTabBar: ElementRef | null = null; + + /** Reference to the portal outlet that we will use to render a TabOutletComponent. */ + @ViewChild(CdkPortalOutlet, { static: true }) + portalOutlet: CdkPortalOutlet | null = null; + + @Output() + tabContentScroll = new EventEmitter(); + + /** The name of the tab group. Used to update the currently active tab in the route */ + @Input() + name = 'tab' + + @Input() + outletClass = ''; + + private scrollTop: number = 0; + + /** Whether or not the current tab should be syncronized with the angular router using a query parameter */ + @Input() + set linkRouter(v: any) { + this._linkRouter = coerceBooleanProperty(v) + } + get linkRouter() { return this._linkRouter } + private _linkRouter = true; + + /** Whether or not the default tab header should be rendered */ + @Input() + set customHeader(v: any) { + this._customHeader = coerceBooleanProperty(v) + } + get customHeader() { return this._customHeader } + private _customHeader = false; + + private tabActivate$ = new Subject(); + private destroyRef = inject(DestroyRef); + + /** Emits the tab QueryList every time there are changes to the content-children */ + get tabs$() { + return this.tabs?.changes + .pipe( + map(() => this.tabs), + startWith(this.tabs) + ) + } + + /** onActivate fires when a tab has been activated. */ + get onActivate(): Observable { return this.tabActivate$.asObservable() } + + /** the index of the currently active tab. */ + activeTabIndex = -1; + + /** The key manager used to support keyboard navigation and type-ahead in the tab group */ + private keymanager: ListKeyManager | null = null; + + /** Used to force the animation direction when calling activateTab. */ + private forceAnimationDirection: 'left' | 'right' | null = null; + + /** + * pendingTabIdx holds the id or the index of a tab that should be activated after the component + * has been bootstrapped. We need to cache this value here because the ActivatedRoute might emit + * before we are AfterViewInit. + */ + private pendingTabIdx: string | null = null; + + constructor( + private injector: Injector, + private route: ActivatedRoute, + private router: Router, + private cdr: ChangeDetectorRef + ) { } + + /** + * @private + * Used to forward keyboard events to the keymanager. + */ + onKeydown(v: KeyboardEvent) { + this.keymanager?.onKeydown(v); + } + + ngOnInit(): void { + this.route.queryParamMap + .pipe( + takeUntilDestroyed(this.destroyRef), + map(params => params.get(this.name)), + distinctUntilChanged(), + ) + .subscribe(newIdx => { + if (!this._linkRouter) { + return; + } + + if (!!this.keymanager && !!this.tabs) { + const actualIndex = this.getIndex(newIdx); + if (actualIndex !== null) { + this.keymanager.setActiveItem(actualIndex); + this.cdr.markForCheck(); + } + } else { + this.pendingTabIdx = newIdx; + } + }) + } + + ngAfterContentInit(): void { + this.keymanager = new ListKeyManager(this.tabs!) + .withHomeAndEnd() + .withHorizontalOrientation("ltr") + .withTypeAhead() + .withWrap() + + this.tabs!.changes + .subscribe(() => { + if (this.portalOutlet?.hasAttached()) { + if (this.tabs!.length === 0) { + this.portalOutlet.detach(); + } + } else { + if (this.tabs!.length > 0) { + this.activateTab(0) + } + } + + }) + + this.keymanager.change + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(change => { + const activeTab = this.tabs!.get(change); + if (!!activeTab && !!activeTab.tabContent) { + const prevIdx = this.activeTabIndex; + + let animationDirection: 'left' | 'right' = prevIdx < change ? 'left' : 'right'; + if (this.forceAnimationDirection !== null) { + animationDirection = this.forceAnimationDirection; + this.forceAnimationDirection = null; + } + + if (this.portalOutlet?.attachedRef) { + // we know for sure that attachedRef is a ComponentRef of TabOutletComponent + const ref = (this.portalOutlet.attachedRef as ComponentRef) + ref.instance._animateDirection = animationDirection; + ref.instance.outletClass = this.outletClass; + ref.changeDetectorRef.detectChanges(); + } + + this.portalOutlet?.detach(); + + const newOutletPortal = this.createTabOutlet(activeTab, animationDirection); + this.activeTabIndex = change; + this.tabContentScroll.next({ + scrollTop: 0, + previousScrollTop: this.scrollTop, + }) + + this.scrollTop = 0; + + this.tabActivate$.next(activeTab.id); + this.portalOutlet?.attach(newOutletPortal); + + this.repositionTabBar(); + + if (this._linkRouter) { + this.router.navigate([], { + queryParams: { + ...this.route.snapshot.queryParams, + [this.name]: this.activeTabIndex, + } + }) + } + this.cdr.markForCheck(); + } + }); + + if (this.pendingTabIdx === null) { + // active the first tab that is NOT disabled + const firstActivatable = this.tabs?.toArray().findIndex(tap => !tap.disabled); + if (firstActivatable !== undefined) { + this.keymanager.setActiveItem(firstActivatable); + } + } else { + const idx = this.getIndex(this.pendingTabIdx); + if (idx !== null) { + this.keymanager.setActiveItem(idx); + this.pendingTabIdx = null; + } + } + } + + ngAfterViewInit(): void { + this.repositionTabBar(); + this.tabHeaders?.changes.subscribe(() => this.repositionTabBar()) + setTimeout(() => this.repositionTabBar(), 250) + } + + /** + * @private + * Activates a new tab + * + * @param idx The index of the new tab. + */ + activateTab(idx: number, forceDirection?: 'left' | 'right') { + if (forceDirection !== undefined) { + this.forceAnimationDirection = forceDirection; + } + + this.keymanager?.setActiveItem(idx); + } + + private getIndex(newIdx: string | null): number | null { + let actualIndex: number = -1; + if (!this.tabs) { + return null; + } + + if (newIdx === undefined || newIdx === null) { // not present in the URL + return null; + } + if (isNaN(+newIdx)) { // likley the ID of a tab + actualIndex = this.tabs?.toArray().findIndex(tab => tab.id === newIdx) || -1; + } else { // it's a number as a string + actualIndex = +newIdx; + } + + if (actualIndex < 0) { + return null; + } + return actualIndex; + } + + private repositionTabBar() { + if (!this.tabHeaders) { + return; + } + + requestAnimationFrame(() => { + const tabHeader = this.tabHeaders!.get(this.activeTabIndex); + if (!tabHeader || !this.activeTabBar) { + return; + } + const rect = tabHeader.nativeElement.getBoundingClientRect(); + const transform = `translate(${tabHeader.nativeElement.offsetLeft}px, ${tabHeader.nativeElement.offsetTop + rect.height}px)` + this.activeTabBar.nativeElement.style.width = `${rect.width}px` + this.activeTabBar.nativeElement.style.transform = transform; + this.activeTabBar.nativeElement.style.opacity = '1'; + + // initialize animations on the active-tab-bar required + if (!this.activeTabBar.nativeElement.classList.contains("transition-all")) { + // only initialize the transitions if this is the very first "reposition" + // this is to prevent the bar from animating to the "bottom" line of the tab + // header the first time. + requestAnimationFrame(() => { + this.activeTabBar?.nativeElement.classList.add("transition-all", "duration-200"); + }) + } + }) + } + + private createTabOutlet(tab: SfngTabComponent, animationDir: 'left' | 'right'): ComponentPortal { + const injector = Injector.create({ + providers: [ + { + provide: TAB_PORTAL, + useValue: tab.tabContent!.portal, + }, + { + provide: TAB_ANIMATION_DIRECTION, + useValue: animationDir, + }, + { + provide: TAB_SCROLL_HANDLER, + useValue: (e: Event) => { + const newScrollTop = (e.target as HTMLElement).scrollTop; + + tab.tabContentScroll.next(e); + this.tabContentScroll.next({ + event: e, + scrollTop: newScrollTop, + previousScrollTop: this.scrollTop, + }); + + this.scrollTop = newScrollTop; + } + }, + ], + parent: this.injector, + name: 'TabOutletInjectot', + }) + + return new ComponentPortal( + TabOutletComponent, + undefined, + injector + ) + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/tabs/tab.ts b/desktop/angular/projects/safing/ui/src/lib/tabs/tab.ts new file mode 100644 index 00000000..31f71226 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tabs/tab.ts @@ -0,0 +1,167 @@ +import { animate, style, transition, trigger } from "@angular/animations"; +import { ListKeyManagerOption } from "@angular/cdk/a11y"; +import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { CdkPortalOutlet, TemplatePortal } from "@angular/cdk/portal"; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, Directive, EventEmitter, Inject, InjectionToken, Input, Output, TemplateRef, ViewChild, ViewContainerRef } from "@angular/core"; + +/** TAB_PORTAL is the injection token used to inject the TabContentDirective portal into TabOutletComponent */ +export const TAB_PORTAL = new InjectionToken('TAB_PORTAL'); + +/** TAB_ANIMATION_DIRECTION is the injection token used to control the :enter animation origin of TabOutletComponent */ +export const TAB_ANIMATION_DIRECTION = new InjectionToken<'left' | 'right'>('TAB_ANIMATION_DIRECTION'); + +/** TAB_SCROLL_HANDLER is called by the SfngTabOutletComponent when a scroll event occurs. */ +export const TAB_SCROLL_HANDLER = new InjectionToken<(_: Event) => void>('TAB_SCROLL_HANDLER') + +/** + * Structural directive (*sfngTabContent) to defined lazy-loaded tab content. + */ +@Directive({ + selector: '[sfngTabContent]', +}) +export class SfngTabContentDirective { + portal: TemplatePortal; + + constructor( + public readonly templateRef: TemplateRef, + public readonly viewRef: ViewContainerRef, + ) { + this.portal = new TemplatePortal(this.templateRef, this.viewRef); + } +} + + +/** + * The tab component that is used to define a new tab as a part of a tab group. + * The content of the tab is lazy-loaded by using the TabContentDirective. + */ +@Component({ + selector: 'sfng-tab', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SfngTabComponent implements ListKeyManagerOption { + @ContentChild(SfngTabContentDirective, { static: false }) + tabContent: SfngTabContentDirective | null = null; + + /** The ID of the tab used to programatically activate the tab. */ + @Input() + id = ''; + + /** The title for the tab as displayed in the tab group header. */ + @Input() + title = ''; + + /** The key for the tip up in the tab group header. */ + @Input() + tipUpKey = ''; + + @Input() + set warning(v) { + this._warning = coerceBooleanProperty(v) + } + get warning() { return this._warning } + private _warning = false; + + /** Emits when the tab content is scrolled */ + @Output() + tabContentScroll = new EventEmitter(); + + /** Whether or not the tab is currently disabled. */ + @Input() + set disabled(v: any) { + this._disabled = coerceBooleanProperty(v); + } + get disabled() { + return this._disabled; + } + private _disabled: boolean = false; + + /** getLabel is used by the list key manager to support type-ahead */ + getLabel() { return this.title } +} + + +/** + * A simple wrapper component around CdkPortalOutlet to add nice + * move animations. + */ +@Component({ + selector: 'sfng-tab-outlet', + template: ` +
+ +
+ `, + styles: [ + ` + :host{ + display: flex; + flex-direction: column; + overflow: hidden; + } + ` + ], + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + trigger( + 'moveInOut', + [ + transition( + ':enter', + [ + style({ opacity: 0, transform: 'translateX({{ in }})' }), + animate('.2s ease-in', + style({ opacity: 1, transform: 'translateX(0%)' })) + ], + { params: { in: '100%' } } // default parameters + ), + transition( + ':leave', + [ + style({ opacity: 1 }), + animate('.2s ease-out', + style({ opacity: 0, transform: 'translateX({{ out }})' })) + ], + { params: { out: '-100%' } } // default parameters + ) + ] + )] +}) +export class TabOutletComponent implements AfterViewInit { + _appAnimate = false; + + @Input() + outletClass = '' + + get in() { + return this._animateDirection == 'left' ? '100%' : '-100%' + } + get out() { + return this._animateDirection == 'left' ? '-100%' : '100%' + } + + onTabContentScroll(event: Event) { + if (!!this.scrollHandler) { + this.scrollHandler(event) + } + } + + @ViewChild(CdkPortalOutlet, { static: true }) + portalOutlet!: CdkPortalOutlet; + + constructor( + @Inject(TAB_PORTAL) public portal: TemplatePortal, + @Inject(TAB_ANIMATION_DIRECTION) public _animateDirection: 'left' | 'right', + @Inject(TAB_SCROLL_HANDLER) public scrollHandler: (_: Event) => void, + private cdr: ChangeDetectorRef + ) { } + + ngAfterViewInit(): void { + this.portalOutlet?.attached + .subscribe(() => { + this._appAnimate = true; + this.cdr.detectChanges(); + }) + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/tabs/tabs.module.ts b/desktop/angular/projects/safing/ui/src/lib/tabs/tabs.module.ts new file mode 100644 index 00000000..e1540cb4 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tabs/tabs.module.ts @@ -0,0 +1,28 @@ +import { PortalModule } from "@angular/cdk/portal"; +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { SfngTipUpModule } from "../tipup"; +import { SfngTabComponent, SfngTabContentDirective, TabOutletComponent } from "./tab"; +import { SfngTabGroupComponent } from "./tab-group"; + +@NgModule({ + imports: [ + CommonModule, + PortalModule, + SfngTipUpModule, + BrowserAnimationsModule + ], + declarations: [ + SfngTabContentDirective, + SfngTabComponent, + SfngTabGroupComponent, + TabOutletComponent, + ], + exports: [ + SfngTabContentDirective, + SfngTabComponent, + SfngTabGroupComponent + ] +}) +export class SfngTabModule { } diff --git a/desktop/angular/projects/safing/ui/src/lib/tipup/_tipup.scss b/desktop/angular/projects/safing/ui/src/lib/tipup/_tipup.scss new file mode 100644 index 00000000..b6b93040 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tipup/_tipup.scss @@ -0,0 +1,52 @@ +sfng-tipup-container { + display: block; + + caption { + @apply text-sm; + opacity: .6; + font-size: .6rem; + } + + h1 { + font-size: 0.85rem; + font-weight: 500; + margin-bottom: 1rem; + } + + .message, + h1 { + flex-shrink: 0; + text-overflow: ellipsis; + word-break: normal; + } + + .message { + font-size: 0.75rem; + flex-grow: 1; + opacity: .8; + max-width: 300px; + padding: 0; + } + + .close-icon { + position: absolute; + top: 1rem; + right: 1rem; + opacity: .7; + cursor: pointer; + + &:hover { + opacity: 1; + } + } + + .buttons { + width: 100%; + display: flex; + justify-content: space-between; + } + + a { + text-decoration: underline; + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/tipup/anchor.ts b/desktop/angular/projects/safing/ui/src/lib/tipup/anchor.ts new file mode 100644 index 00000000..986d0378 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tipup/anchor.ts @@ -0,0 +1,43 @@ +import { Directive, ElementRef, HostBinding, Input, isDevMode } from "@angular/core"; +import { SfngTipUpPlacement } from "./utils"; + +@Directive({ + selector: '[sfngTipUpAnchor]', +}) +export class SfngTipUpAnchorDirective implements SfngTipUpPlacement { + constructor( + public readonly elementRef: ElementRef, + ) { } + + origin: 'left' | 'right' = 'right'; + offset: number = 10; + + @HostBinding('class.active-tipup-anchor') + isActiveAnchor = false; + + @Input() + set sfngTipUpAnchor(posSpec: string | undefined) { + const parts = (posSpec || '').split(';') + if (parts.length > 2) { + if (isDevMode()) { + throw new Error(`Invalid value "${posSpec}" for [sfngTipUpAnchor]`); + } + return; + } + + if (parts[0] === 'left') { + this.origin = 'left'; + } else { + this.origin = 'right'; + } + + if (parts.length === 2) { + this.offset = +parts[1]; + if (isNaN(this.offset)) { + this.offset = 10; + } + } else { + this.offset = 10; + } + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/tipup/clone-node.ts b/desktop/angular/projects/safing/ui/src/lib/tipup/clone-node.ts new file mode 100644 index 00000000..e0550060 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tipup/clone-node.ts @@ -0,0 +1,128 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** Creates a deep clone of an element. */ +export function deepCloneNode(node: HTMLElement): HTMLElement { + const clone = node.cloneNode(true) as HTMLElement; + const descendantsWithId = clone.querySelectorAll('[id]'); + const nodeName = node.nodeName.toLowerCase(); + + // Remove the `id` to avoid having multiple elements with the same id on the page. + clone.removeAttribute('id'); + + for (let i = 0; i < descendantsWithId.length; i++) { + descendantsWithId[i].removeAttribute('id'); + } + + if (nodeName === 'canvas') { + transferCanvasData(node as HTMLCanvasElement, clone as HTMLCanvasElement); + } else if (nodeName === 'input' || nodeName === 'select' || nodeName === 'textarea') { + transferInputData(node as HTMLInputElement, clone as HTMLInputElement); + } + + transferData('canvas', node, clone, transferCanvasData); + transferData('input, textarea, select', node, clone, transferInputData); + return clone; +} + +/** Matches elements between an element and its clone and allows for their data to be cloned. */ +function transferData(selector: string, node: HTMLElement, clone: HTMLElement, + callback: (source: T, clone: T) => void) { + const descendantElements = node.querySelectorAll(selector); + + if (descendantElements.length) { + const cloneElements = clone.querySelectorAll(selector); + + for (let i = 0; i < descendantElements.length; i++) { + callback(descendantElements[i], cloneElements[i]); + } + } +} + +// Counter for unique cloned radio button names. +let cloneUniqueId = 0; + +/** Transfers the data of one input element to another. */ +function transferInputData(source: Element & { value: string }, + clone: Element & { value: string; name: string; type: string }) { + // Browsers throw an error when assigning the value of a file input programmatically. + if (clone.type !== 'file') { + clone.value = source.value; + } + + // Radio button `name` attributes must be unique for radio button groups + // otherwise original radio buttons can lose their checked state + // once the clone is inserted in the DOM. + if (clone.type === 'radio' && clone.name) { + clone.name = `sfng-clone-${clone.name}-${cloneUniqueId++}`; + } +} + +/** Transfers the data of one canvas element to another. */ +function transferCanvasData(source: HTMLCanvasElement, clone: HTMLCanvasElement) { + const context = clone.getContext('2d'); + + if (context) { + // In some cases `drawImage` can throw (e.g. if the canvas size is 0x0). + // We can't do much about it so just ignore the error. + try { + context.drawImage(source, 0, 0); + } catch { } + } +} + +/** + * Gets a 3d `transform` that can be applied to an element. + * @param x Desired position of the element along the X axis. + * @param y Desired position of the element along the Y axis. + */ +export function getTransform(x: number, y: number): string { + // Round the transforms since some browsers will + // blur the elements for sub-pixel transforms. + return `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)`; +} + +/** + * Matches the target element's size to the source's size. + * @param target Element that needs to be resized. + * @param sourceRect Dimensions of the source element. + */ +export function matchElementSize(target: HTMLElement, sourceRect: ClientRect): void { + target.style.width = `${sourceRect.width}px`; + target.style.height = `${sourceRect.height}px`; + target.style.transform = getTransform(sourceRect.left, sourceRect.top); +} + +/** + * Shallow-extends a stylesheet object with another stylesheet-like object. + * Note that the keys in `source` have to be dash-cased. + */ +export function extendStyles(dest: CSSStyleDeclaration, + source: Record, + importantProperties?: Set) { + for (let key in source) { + if (source.hasOwnProperty(key)) { + const value = source[key]; + + if (value) { + dest.setProperty(key, value, importantProperties?.has(key) ? 'important' : ''); + } else { + dest.removeProperty(key); + } + } + } + + return dest; +} + +export function removeNode(node: Node | null) { + if (node && node.parentNode) { + node.parentNode.removeChild(node); + } +} + diff --git a/desktop/angular/projects/safing/ui/src/lib/tipup/css-utils.ts b/desktop/angular/projects/safing/ui/src/lib/tipup/css-utils.ts new file mode 100644 index 00000000..8f58dff2 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tipup/css-utils.ts @@ -0,0 +1,87 @@ + +export function synchronizeCssStyles(src: HTMLElement, destination: HTMLElement, skipStyles: Set) { + // Get a list of all the source and destination elements + const srcElements = >src.getElementsByTagName('*'); + const dstElements = >destination.getElementsByTagName('*'); + + cloneStyle(src, destination, skipStyles); + + // For each element + for (let i = srcElements.length; i--;) { + const srcElement = srcElements[i]; + const dstElement = dstElements[i]; + cloneStyle(srcElement, dstElement, skipStyles); + } +} + +function cloneStyle(srcElement: HTMLElement, dstElement: HTMLElement, skipStyles: Set) { + const sourceElementStyles = document.defaultView!.getComputedStyle(srcElement, ''); + const styleAttributeKeyNumbers = Object.keys(sourceElementStyles); + + // Copy the attribute + for (let j = 0; j < styleAttributeKeyNumbers.length; j++) { + const attributeKeyNumber = styleAttributeKeyNumbers[j]; + const attributeKey: string = sourceElementStyles[attributeKeyNumber as any]; + if (!isNaN(+attributeKey)) { + continue + } + if (attributeKey === 'cssText') { + continue + } + + if (skipStyles.has(attributeKey)) { + continue + } + + try { + dstElement.style[attributeKey as any] = sourceElementStyles[attributeKey as any]; + } catch (e) { + console.error(attributeKey, e); + } + } +} + +/** + * Returns a CSS selector for el from rootNode. + * + * @param el The source element to get the CSS path to + * @param rootNode The root node at which the CSS path should be applyable + * @returns A CSS selector to access el from rootNode. + */ +export function getCssSelector(el: HTMLElement, rootNode: HTMLElement | null): string { + if (!el) { + return ''; + } + let stack = []; + let isShadow = false; + while (el !== rootNode && el.parentNode !== null) { + // console.log(el.nodeName); + let sibCount = 0; + let sibIndex = 0; + // get sibling indexes + for (let i = 0; i < (el.parentNode as HTMLElement).childNodes.length; i++) { + let sib = (el.parentNode as HTMLElement).childNodes[i]; + if (sib.nodeName == el.nodeName) { + if (sib === el) { + sibIndex = sibCount; + } + sibCount++; + } + } + let nodeName = el.nodeName.toLowerCase(); + if (isShadow) { + throw new Error(`cannot traverse into shadow dom.`) + } + if (sibCount > 1) { + stack.unshift(nodeName + ':nth-of-type(' + (sibIndex + 1) + ')'); + } else { + stack.unshift(nodeName); + } + el = el.parentNode as HTMLElement; + if (el.nodeType === 11) { // for shadow dom, we + isShadow = true; + el = (el as any).host; + } + } + return stack.join(' > '); +} diff --git a/desktop/angular/projects/safing/ui/src/lib/tipup/index.ts b/desktop/angular/projects/safing/ui/src/lib/tipup/index.ts new file mode 100644 index 00000000..bd600272 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tipup/index.ts @@ -0,0 +1,6 @@ +export * from './anchor'; +export * from './tipup'; +export * from './tipup-component'; +export * from './tipup.module'; +export * from './translations'; + diff --git a/desktop/angular/projects/safing/ui/src/lib/tipup/safe.pipe.ts b/desktop/angular/projects/safing/ui/src/lib/tipup/safe.pipe.ts new file mode 100644 index 00000000..0cbf2855 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tipup/safe.pipe.ts @@ -0,0 +1,21 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DomSanitizer, SafeHtml, SafeStyle, SafeScript, SafeUrl, SafeResourceUrl } from '@angular/platform-browser'; + +@Pipe({ + name: 'safe' +}) +export class SafePipe implements PipeTransform { + + constructor(protected sanitizer: DomSanitizer) { } + + public transform(value: any, type: string): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl { + switch (type) { + case 'html': return this.sanitizer.bypassSecurityTrustHtml(value); + case 'style': return this.sanitizer.bypassSecurityTrustStyle(value); + case 'script': return this.sanitizer.bypassSecurityTrustScript(value); + case 'url': return this.sanitizer.bypassSecurityTrustUrl(value); + case 'resourceUrl': return this.sanitizer.bypassSecurityTrustResourceUrl(value); + default: throw new Error(`Invalid safe type specified: ${type}`); + } + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/tipup/tipup-component.ts b/desktop/angular/projects/safing/ui/src/lib/tipup/tipup-component.ts new file mode 100644 index 00000000..8747b098 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tipup/tipup-component.ts @@ -0,0 +1,67 @@ +import { ChangeDetectionStrategy, Component, Inject, OnInit } from "@angular/core"; +import { SfngDialogRef, SFNG_DIALOG_REF } from "../dialog"; +import { SfngTipUpService } from "./tipup"; +import { ActionRunner, Button, SFNG_TIP_UP_ACTION_RUNNER, TipUp } from './translations'; +import { TIPUP_TOKEN } from "./utils"; + +@Component({ + selector: 'sfng-tipup-container', + templateUrl: './tipup.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SfngTipUpComponent implements OnInit, TipUp { + title: string = 'N/A'; + content: string = 'N/A'; + nextKey?: string; + buttons?: Button[]; + url?: string; + urlText: string = 'Read More'; + + constructor( + @Inject(TIPUP_TOKEN) public readonly token: string, + @Inject(SFNG_DIALOG_REF) private readonly dialogRef: SfngDialogRef, + @Inject(SFNG_TIP_UP_ACTION_RUNNER) private runner: ActionRunner, + private tipupService: SfngTipUpService, + ) { } + + ngOnInit() { + const doc = this.tipupService.getTipUp(this.token); + if (!!doc) { + Object.assign(this, doc); + this.urlText = doc.urlText || 'Read More'; + } + } + + async next() { + if (!this.nextKey) { + return; + } + + this.tipupService.open(this.nextKey); + this.dialogRef.close(); + } + + async runAction(btn: Button) { + await this.runner.performAction(btn.action); + + // if we have a nextKey for the button but do not do in-app + // routing we should be able to open the next tipup as soon + // as the action finished + if (!!btn.nextKey) { + this.tipupService.waitFor(btn.nextKey!) + .subscribe({ + next: () => { + this.dialogRef.close(); + this.tipupService.open(btn.nextKey!); + }, + error: console.error + }) + } else { + this.close(); + } + } + + close() { + this.dialogRef.close(); + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/tipup/tipup.html b/desktop/angular/projects/safing/ui/src/lib/tipup/tipup.html new file mode 100644 index 00000000..ac54fe8a --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tipup/tipup.html @@ -0,0 +1,22 @@ +
diff --git a/desktop/angular/projects/safing/ui/src/lib/tipup/tipup.module.ts b/desktop/angular/projects/safing/ui/src/lib/tipup/tipup.module.ts new file mode 100644 index 00000000..42378c6f --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tipup/tipup.module.ts @@ -0,0 +1,47 @@ +import { CommonModule } from "@angular/common"; +import { ModuleWithProviders, NgModule, Type } from "@angular/core"; +import { MarkdownModule } from "ngx-markdown"; +import { SfngDialogModule } from "../dialog"; +import { SfngTipUpAnchorDirective } from './anchor'; +import { SfngsfngTipUpTriggerDirective, SfngTipUpIconComponent } from './tipup'; +import { SfngTipUpComponent } from './tipup-component'; +import { ActionRunner, HelpTexts, SFNG_TIP_UP_ACTION_RUNNER, SFNG_TIP_UP_CONTENTS } from "./translations"; +import { SafePipe } from "./safe.pipe"; + +@NgModule({ + imports: [ + CommonModule, + MarkdownModule.forChild(), + SfngDialogModule, + ], + declarations: [ + SfngTipUpIconComponent, + SfngsfngTipUpTriggerDirective, + SfngTipUpComponent, + SfngTipUpAnchorDirective, + SafePipe + ], + exports: [ + SfngTipUpIconComponent, + SfngsfngTipUpTriggerDirective, + SfngTipUpComponent, + SfngTipUpAnchorDirective + ], +}) +export class SfngTipUpModule { + static forRoot(text: HelpTexts, runner: Type>): ModuleWithProviders { + return { + ngModule: SfngTipUpModule, + providers: [ + { + provide: SFNG_TIP_UP_CONTENTS, + useValue: text, + }, + { + provide: SFNG_TIP_UP_ACTION_RUNNER, + useExisting: runner, + } + ] + } + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/tipup/tipup.ts b/desktop/angular/projects/safing/ui/src/lib/tipup/tipup.ts new file mode 100644 index 00000000..7f6fbd85 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tipup/tipup.ts @@ -0,0 +1,526 @@ +/* eslint-disable @angular-eslint/no-input-rename */ +import { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion'; +import { ConnectedPosition } from '@angular/cdk/overlay'; +import { _getShadowRoot } from '@angular/cdk/platform'; +import { DOCUMENT } from '@angular/common'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Directive, ElementRef, HostBinding, HostListener, Inject, Injectable, Injector, Input, NgZone, OnDestroy, Optional, Renderer2, RendererFactory2 } from '@angular/core'; +import { Observable, of, Subject } from 'rxjs'; +import { debounce, debounceTime, filter, map, skip, take, timeout } from 'rxjs/operators'; +import { SfngDialogRef, SfngDialogService } from '../dialog'; +import { SfngTipUpAnchorDirective } from './anchor'; +import { deepCloneNode, extendStyles, matchElementSize, removeNode } from './clone-node'; +import { getCssSelector, synchronizeCssStyles } from './css-utils'; +import { SfngTipUpComponent } from './tipup-component'; +import { Button, HelpTexts, SFNG_TIP_UP_CONTENTS, TipUp } from './translations'; +import { SfngTipUpPlacement, TIPUP_TOKEN } from './utils'; + +@Directive({ + selector: '[sfngTipUpTrigger]', +}) +export class SfngsfngTipUpTriggerDirective implements OnDestroy { + constructor( + public readonly elementRef: ElementRef, + public dialog: SfngDialogService, + @Optional() @Inject(SfngTipUpAnchorDirective) public anchor: SfngTipUpAnchorDirective | ElementRef | HTMLElement, + @Inject(SFNG_TIP_UP_CONTENTS) private tipUpContents: HelpTexts, + private tipupService: SfngTipUpService, + private cdr: ChangeDetectorRef, + ) { } + + private dialogRef: SfngDialogRef | null = null; + + /** + * The helptext token used to search for the tip up defintion. + */ + @Input('sfngTipUpTrigger') + set textKey(s: string) { + if (!!this._textKey) { + this.tipupService.deregister(this._textKey, this); + } + this._textKey = s; + this.tipupService.register(this._textKey, this); + } + get textKey() { return this._textKey; } + private _textKey: string = ''; + + /** + * The text to display inside the tip up. If unset, the tipup definition + * will be loaded form helptexts.yaml. + * This input property is mainly designed for programatic/dynamic tip-up generation + */ + @Input('sfngTipUpText') + text: string | undefined; + + @Input('sfngTipUpTitle') + title: string | undefined; + + @Input('sfngTipUpButtons') + buttons: Button[] | undefined; + + /** + * asTipUp returns a tip-up definition built from the input + * properties sfngTipUpText and sfngTipUpTitle. If none are set + * then null is returned. + */ + asTipUp(): TipUp | null { + // TODO(ppacher): we could also merge the defintions from MyYamlFile + // and the properties set on this directive.... + if (!this.text) { + return this.tipUpContents[this.textKey]; + } + return { + title: this.title || '', + content: this.text, + buttons: this.buttons, + } + } + + /** + * The default anchor for the tipup if non is provided via Dependency-Injection + * or using sfngTipUpAnchorRef + */ + @Input('sfngTipUpDefaultAnchor') + defaultAnchor: ElementRef | HTMLElement | null = null; + + /** Optionally overwrite the anchor element received via Dependency Injection */ + @Input('sfngTipUpAnchorRef') + set anchorRef(ref: ElementRef | HTMLElement | null) { + this.anchor = ref ?? this.anchor; + } + + /** Used to ensure all tip-up triggers have a pointer cursor */ + @HostBinding('style.cursor') + cursor = 'pointer'; + + /** De-register ourself upon destroy */ + ngOnDestroy() { + this.tipupService.deregister(this.textKey, this); + } + + /** Whether or not we're passive-only and thus do not handle click-events form the user */ + @Input('sfngTipUpPassive') + set passive(v: any) { + this._passive = coerceBooleanProperty(v ?? true); + } + get passive() { return this._passive; } + private _passive = false; + + @Input('sfngTipUpOffset') + set offset(v: any) { + this._defaultOffset = coerceNumberProperty(v) + } + get offset() { return this._defaultOffset } + private _defaultOffset = 20; + + @Input('sfngTipUpPlacement') + placement: SfngTipUpPlacement | null = null; + + @HostListener('click', ['$event']) + onClick(event?: MouseEvent): Promise { + if (!!event) { + // if there's a click event the user actually clicked the element. + // we only handle this if we're not marked as passive. + if (this._passive) { + return Promise.resolve(); + } + + event.preventDefault(); + event.stopPropagation(); + } + + if (!!this.dialogRef) { + this.dialogRef.close(); + return Promise.resolve(); + } + + let anchorElement: ElementRef | HTMLElement | null = this.defaultAnchor || this.elementRef; + let placement: SfngTipUpPlacement | null = this.placement; + + if (!!this.anchor) { + if (this.anchor instanceof SfngTipUpAnchorDirective) { + anchorElement = this.anchor.elementRef; + placement = this.anchor; + } else { + anchorElement = this.anchor; + } + } + + this.dialogRef = this.tipupService.createTipup( + anchorElement, + this.textKey, + this, + placement, + ) + + this.dialogRef.onClose + .pipe(take(1)) + .subscribe(() => { + this.dialogRef = null; + this.cdr.markForCheck(); + }); + + this.cdr.detectChanges(); + + return this.dialogRef.onStateChange + .pipe( + filter(state => state === 'opening'), + take(1), + ) + .toPromise() + } +} + +@Component({ + selector: 'sfng-tipup', + template: + ` + + + + + `, + styles: [ + ` + :host { + display: inline-block; + width : 1rem; + position: relative; + opacity: 0.55; + cursor : pointer; + align-self: center; + } + + :host:hover { + opacity: 1; + } + ` + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SfngTipUpIconComponent implements SfngTipUpPlacement { + @Input() + key: string = ''; + + // see sfngTipUpTrigger sfngTipUpText and sfngTipUpTitle + @Input() text: string | undefined = undefined; + @Input() title: string | undefined = undefined; + @Input() buttons: Button[] | undefined = undefined; + + @Input() + anchor: ElementRef | HTMLElement | null = null; + + @Input('placement') + origin: 'left' | 'right' = 'right'; + + @Input() + set offset(v: any) { + this._offset = coerceNumberProperty(v); + } + get offset() { return this._offset; } + private _offset: number = 10; + + constructor(private elementRef: ElementRef) { } + + get placement(): SfngTipUpPlacement { + return this + } + + get parent(): HTMLElement | null { + return (this.elementRef?.nativeElement as HTMLElement)?.parentElement; + } +} + + +@Injectable({ + providedIn: 'root' +}) +export class SfngTipUpService { + tipups = new Map(); + + private _onRegister = new Subject(); + private _onUnregister = new Subject(); + + get onRegister(): Observable { + return this._onRegister.asObservable(); + } + + get onUnregister(): Observable { + return this._onUnregister.asObservable(); + } + + waitFor(key: string): Observable { + if (this.tipups.has(key)) { + return of(undefined); + } + + return this.onRegister + .pipe( + filter(val => val === key), + debounce(() => this.ngZone.onStable.pipe(skip(2))), + debounceTime(1000), + take(1), + map(() => { }), + timeout(5000), + ); + } + + private renderer: Renderer2; + + constructor( + @Inject(DOCUMENT) private _document: Document, + private dialog: SfngDialogService, + private ngZone: NgZone, + private injector: Injector, + rendererFactory: RendererFactory2 + ) { + this.renderer = rendererFactory.createRenderer(null, null) + } + + register(key: string, trigger: SfngsfngTipUpTriggerDirective) { + if (this.tipups.has(key)) { + return; + } + + this.tipups.set(key, trigger); + this._onRegister.next(key); + } + + deregister(key: string, trigger: SfngsfngTipUpTriggerDirective) { + if (this.tipups.get(key) === trigger) { + this.tipups.delete(key); + this._onUnregister.next(key); + } + } + + getTipUp(key: string): TipUp | null { + return this.tipups.get(key)?.asTipUp() || null; + } + + private _latestTipUp: SfngDialogRef | null = null; + + createTipup( + anchor: HTMLElement | ElementRef, + key: string, + origin?: SfngsfngTipUpTriggerDirective, + opts: SfngTipUpPlacement | null = {}, + injector?: Injector): SfngDialogRef { + + const lastTipUp = this._latestTipUp + let closePrevious = () => { + if (!!lastTipUp) { + lastTipUp.close(); + } + } + + // make sure we have an ElementRef to work with + if (!(anchor instanceof ElementRef)) { + anchor = new ElementRef(anchor) + } + + // the the origin placement of the tipup + const positions: ConnectedPosition[] = []; + if (opts?.origin === 'left') { + positions.push({ + originX: 'start', + originY: 'center', + overlayX: 'end', + overlayY: 'center', + }) + } else { + positions.push({ + originX: 'end', + originY: 'center', + overlayX: 'start', + overlayY: 'center', + }) + } + + // determine the offset to the tipup origin + let offset = opts?.offset ?? 10; + if (opts?.origin === 'left') { + offset *= -1; + } + + let postitionStrategy = this.dialog.position() + .flexibleConnectedTo(anchor) + .withPositions(positions) + .withDefaultOffsetX(offset); + + const inj = Injector.create({ + providers: [ + { + useValue: key, + provide: TIPUP_TOKEN, + } + ], + parent: injector || this.injector, + }); + + + const newTipUp = this.dialog.create(SfngTipUpComponent, { + dragable: false, + autoclose: true, + backdrop: 'light', + injector: inj, + positionStrategy: postitionStrategy + }); + this._latestTipUp = newTipUp; + + const _preview = this._createPreview(anchor.nativeElement, _getShadowRoot(anchor.nativeElement)); + + // construct a CSS selector that targets the clicked origin (sfngTipUpTriggerDirective) from within + // the anchor. We use that path to highlight the copy of the trigger-directive in the preview. + if (!!origin) { + const originSelector = getCssSelector(origin.elementRef.nativeElement, anchor.nativeElement); + let target: HTMLElement | null = null; + if (!!originSelector) { + target = _preview.querySelector(originSelector); + } else { + target = _preview; + } + + this.renderer.addClass(target, 'active-tipup-trigger') + } + + newTipUp.onStateChange + .pipe( + filter(state => state === 'open'), + take(1) + ) + .subscribe(() => { + closePrevious(); + _preview.attach() + }) + + newTipUp.onStateChange + .pipe( + filter(state => state === 'closing'), + take(1) + ) + .subscribe(() => { + if (this._latestTipUp === newTipUp) { + this._latestTipUp = null; + } + _preview.classList.remove('visible'); + setTimeout(() => { + removeNode(_preview); + }, 300) + }); + + return newTipUp; + } + + private _createPreview(element: HTMLElement, shadowRoot: ShadowRoot | null): HTMLElement & { attach: () => void } { + const preview = deepCloneNode(element); + // clone all CSS styles by applying them directly to the copied + // nodes. Though, we skip the opacity property because we use that + // a lot and it makes the preview strange .... + synchronizeCssStyles(element, preview, new Set([ + 'opacity' + ])); + + // make sure the preview element is at the exact same position + // as the original one. + matchElementSize(preview, element.getBoundingClientRect()); + + extendStyles(preview.style, { + // We have to reset the margin, because it can throw off positioning relative to the viewport. + 'margin': '0', + 'position': 'fixed', + 'top': '0', + 'left': '0', + 'z-index': '1000', + 'opacity': 'unset', + }, new Set(['position'])); + + // We add a dedicated class to the preview element so + // it can handle special higlighting itself. + preview.classList.add('tipup-preview') + + // since the user might want to click on the preview element we must + // intercept the click-event, determine the path to the target element inside + // the preview and eventually dispatch a click-event on the actual + // - real - target inside the cloned element. + preview.onclick = function (event: MouseEvent) { + let path = getCssSelector(event.target as HTMLElement, preview); + if (!!path) { + // find the target by it's CSS path + let actualTarget: HTMLElement | null = element.querySelector(path); + + // some (SVG) elements don't have a direct click() listener so we need to search + // the parents upwards to find one that implements click(). + // we're basically searching up until we reach the tag. + // + // TODO(ppacher): stop searching at the respective root node. + if (!!actualTarget) { + let iter: HTMLElement = actualTarget; + while (iter != null) { + if ('click' in iter && typeof iter['click'] === 'function') { + iter.click(); + break; + } + iter = iter.parentNode as HTMLElement; + } + } + } else { + // the user clicked the preview element directly + try { + element.click() + } catch (e) { + console.error(e); + } + } + } + + let attach = () => { + const parent = this._getPreviewInserationPoint(shadowRoot) + const cdkOverlayContainer = parent.getElementsByClassName('cdk-overlay-container')[0] + // if we find a cdkOverlayContainer in our inseration point (which we expect to be there) + // we insert the preview element right after the overlay-backdrop. This way the tip-up + // dialog will still be on top of the preview. + if (!!cdkOverlayContainer) { + const reference = cdkOverlayContainer.getElementsByClassName("cdk-overlay-backdrop")[0].nextSibling; + cdkOverlayContainer.insertBefore(preview, reference) + } else { + parent.appendChild(preview); + } + + setTimeout(() => { + preview.classList.add('visible'); + }) + } + + Object.defineProperty(preview, 'attach', { + value: attach, + }) + + return preview as any; + } + + private _getPreviewInserationPoint(shadowRoot: ShadowRoot | null): HTMLElement { + const documentRef = this._document; + return shadowRoot || + documentRef.fullscreenElement || + (documentRef as any).webkitFullscreenElement || + (documentRef as any).mozFullScreenElement || + (documentRef as any).msFullscreenElement || + documentRef.body; + } + + async open(key: string) { + const comp = this.tipups.get(key); + if (!comp) { + console.error('Tried to open unknown tip-up with key ' + key); + return; + } + comp.onClick() + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/tipup/translations.ts b/desktop/angular/projects/safing/ui/src/lib/tipup/translations.ts new file mode 100644 index 00000000..fdc0ecd5 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tipup/translations.ts @@ -0,0 +1,27 @@ +import { InjectionToken } from '@angular/core'; + +export const SFNG_TIP_UP_CONTENTS = new InjectionToken>('SfngTipUpContents'); +export const SFNG_TIP_UP_ACTION_RUNNER = new InjectionToken>('SfngTipUpActionRunner') + +export interface Button { + name: string; + action: T; + nextKey?: string; +} + +export interface TipUp { + title: string; + content: string; + url?: string; + urlText?: string; + buttons?: Button[]; + nextKey?: string; +} + +export interface HelpTexts { + [key: string]: TipUp; +} + +export interface ActionRunner { + performAction(action: T): Promise; +} diff --git a/desktop/angular/projects/safing/ui/src/lib/tipup/utils.ts b/desktop/angular/projects/safing/ui/src/lib/tipup/utils.ts new file mode 100644 index 00000000..7ccffbd4 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tipup/utils.ts @@ -0,0 +1,8 @@ +import { InjectionToken } from "@angular/core"; + +export const TIPUP_TOKEN = new InjectionToken('TipUPJSONToken'); + +export interface SfngTipUpPlacement { + origin?: 'left' | 'right'; + offset?: number; +} diff --git a/desktop/angular/projects/safing/ui/src/lib/toggle-switch/_toggle-switch.scss b/desktop/angular/projects/safing/ui/src/lib/toggle-switch/_toggle-switch.scss new file mode 100644 index 00000000..246a7953 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/toggle-switch/_toggle-switch.scss @@ -0,0 +1,35 @@ +sfng-toggle { + @apply flex items-center; + + label { + @apply inline-block w-10 h-5 relative bg-gray-500 rounded-full; + } + + .slider { + @apply absolute cursor-pointer top-0 left-0 right-0 bottom-0 bg-gray-600 transition-all duration-100 rounded-full shadow-inner-xs; + } + + .dot { + @apply absolute transition-all duration-200 rounded-full bg-white; + height: 18px; + width: 18px; + bottom: 1px; + left: 1px; + } + + input:checked:not(:disabled)+.slider { + @apply bg-green-300 bg-opacity-50 text-green; + } + + input:disabled+.slider { + @apply opacity-75 cursor-not-allowed; + } + + .dot.checked { + transform: translateX(calc(2.5rem - 18px - 2px)); + } + + .dot.disabled { + transform: translateX(calc((2.5rem - 18px - 2px)/2)); + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/toggle-switch/index.ts b/desktop/angular/projects/safing/ui/src/lib/toggle-switch/index.ts new file mode 100644 index 00000000..fbc94093 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/toggle-switch/index.ts @@ -0,0 +1,3 @@ +export * from './toggle-switch'; +export * from './toggle.module'; + diff --git a/desktop/angular/projects/safing/ui/src/lib/toggle-switch/toggle-switch.html b/desktop/angular/projects/safing/ui/src/lib/toggle-switch/toggle-switch.html new file mode 100644 index 00000000..69320c3a --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/toggle-switch/toggle-switch.html @@ -0,0 +1,20 @@ + diff --git a/desktop/angular/projects/safing/ui/src/lib/toggle-switch/toggle-switch.ts b/desktop/angular/projects/safing/ui/src/lib/toggle-switch/toggle-switch.ts new file mode 100644 index 00000000..6b90f961 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/toggle-switch/toggle-switch.ts @@ -0,0 +1,59 @@ +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, HostListener } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +@Component({ + selector: 'sfng-toggle', + templateUrl: './toggle-switch.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SfngToggleSwitchComponent), + multi: true, + } + ] +}) +export class SfngToggleSwitchComponent implements ControlValueAccessor { + @HostListener('blur') + onBlur() { + this.onTouch(); + } + + set disabled(v: any) { + this.setDisabledState(coerceBooleanProperty(v)) + } + get disabled() { + return this._disabled; + } + private _disabled = false; + + value: boolean = false; + + constructor(private _changeDetector: ChangeDetectorRef) { } + + setDisabledState(isDisabled: boolean) { + this._disabled = isDisabled; + this._changeDetector.markForCheck(); + } + + onValueChange(value: boolean) { + this.value = value; + this.onChange(this.value); + } + + writeValue(value: boolean) { + this.value = value; + this._changeDetector.markForCheck(); + } + + onChange = (_: any): void => { }; + registerOnChange(fn: (value: any) => void) { + this.onChange = fn; + } + + onTouch = (): void => { }; + registerOnTouched(fn: () => void) { + this.onTouch = fn; + } +} diff --git a/desktop/angular/projects/safing/ui/src/lib/toggle-switch/toggle.module.ts b/desktop/angular/projects/safing/ui/src/lib/toggle-switch/toggle.module.ts new file mode 100644 index 00000000..db27249b --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/toggle-switch/toggle.module.ts @@ -0,0 +1,18 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { SfngToggleSwitchComponent } from "./toggle-switch"; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ], + declarations: [ + SfngToggleSwitchComponent, + ], + exports: [ + SfngToggleSwitchComponent, + ] +}) +export class SfngToggleSwitchModule { } diff --git a/desktop/angular/projects/safing/ui/src/lib/tooltip/_tooltip-component.scss b/desktop/angular/projects/safing/ui/src/lib/tooltip/_tooltip-component.scss new file mode 100644 index 00000000..ff90d82a --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tooltip/_tooltip-component.scss @@ -0,0 +1,5 @@ +sfng-tooltip-container { + @apply relative block; + + max-width: 16rem; +} diff --git a/desktop/angular/projects/safing/ui/src/lib/tooltip/index.ts b/desktop/angular/projects/safing/ui/src/lib/tooltip/index.ts new file mode 100644 index 00000000..fb071730 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tooltip/index.ts @@ -0,0 +1,3 @@ +export * from './tooltip'; +export * from './tooltip.module'; + diff --git a/desktop/angular/projects/safing/ui/src/lib/tooltip/tooltip-component.html b/desktop/angular/projects/safing/ui/src/lib/tooltip/tooltip-component.html new file mode 100644 index 00000000..ad68d95c --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tooltip/tooltip-component.html @@ -0,0 +1,6 @@ +
+ {{ message }} + +
diff --git a/desktop/angular/projects/safing/ui/src/lib/tooltip/tooltip-component.ts b/desktop/angular/projects/safing/ui/src/lib/tooltip/tooltip-component.ts new file mode 100644 index 00000000..a206d5b3 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tooltip/tooltip-component.ts @@ -0,0 +1,139 @@ +import { animate, AnimationEvent, style, transition, trigger } from "@angular/animations"; +import { OverlayRef } from "@angular/cdk/overlay"; +import { TemplatePortal } from "@angular/cdk/portal"; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostBinding, HostListener, Inject, InjectionToken, OnDestroy, TemplateRef, ViewContainerRef } from "@angular/core"; +import { SfngTooltipDirective } from "./tooltip"; + +export const SFNG_TOOLTIP_CONTENT = new InjectionToken>('SFNG_TOOLTIP_CONTENT'); +export const SFNG_TOOLTIP_OVERLAY = new InjectionToken('SFNG_TOOLTIP_OVERLAY'); + +@Component({ + selector: 'sfng-tooltip-container', + templateUrl: './tooltip-component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + trigger( + 'moveInOut', + [ + transition( + ':enter', + [ + style({ opacity: 0, transform: 'translate{{ what }}({{ value }}) scale(0.75)' }), + animate('.1s ease-in', + style({ opacity: 1, transform: 'translate{{ what }}(0%) scale(1)' })) + ], + { params: { what: 'Y', value: '-8px' } } // default parameters + ), + transition( + ':leave', + [ + style({ opacity: 1 }), + animate('.1s ease-out', + style({ opacity: 0, transform: 'translate{{ what }}({{ value }}) scale(0.75)' })) + ], + { params: { what: 'Y', value: '8px' } } // default parameters + ) + ] + )] + +}) +export class SfngTooltipComponent implements AfterViewInit, OnDestroy { + /** + * Adds snfg-tooltip-instance class to the host element. + * This is used as a selector in the FlexibleConnectedPosition stragegy + * to set a transform-origin. That origin is then used for the "arrow" anchor + * placement. + */ + @HostBinding('class.sfng-tooltip-instance') + _hostClass = true; + + /** + * Used to clear the "hide" timeout when the cursor moves from the the origin + * into the tooltip content. + * This is required if the tooltip contains rich and likely clickable content. + */ + @HostListener('mouseenter') + onMouseEnter() { this.directive.show() } + + /** + * If the tooltip is visible because the user moved inside the tooltip-component + * (see comment above) then we need to handle a mouse-leave event as well. + */ + @HostListener('mouseleave') + onMouseLeave() { this.directive.hide() } + + what = 'Y'; + value = '8px' + transformOrigin = ''; + + _appAnimate = false; + + private observer: MutationObserver | null = null; + + /** Message is the tooltip message to display in case tooltipContent is a string */ + message = ''; + + /** Portal is the tooltip content to display in case tooltipContent is a template reference */ + portal: TemplatePortal | null = null; + + constructor( + @Inject(SFNG_TOOLTIP_CONTENT) tooltipContent: string | TemplateRef, + @Inject(SFNG_TOOLTIP_OVERLAY) public overlayRef: OverlayRef, + private directive: SfngTooltipDirective, + private elementRef: ElementRef, + private cdr: ChangeDetectorRef, + private viewContainer: ViewContainerRef + ) { + if (tooltipContent instanceof TemplateRef) { + this.portal = new TemplatePortal(tooltipContent, this.viewContainer) + } else { + this.message = tooltipContent; + } + } + + dispose() { + this._appAnimate = false; + this.cdr.markForCheck(); + } + + animationDone(event: AnimationEvent) { + if (event.toState === 'void') { + this.overlayRef.dispose(); + } + } + + ngOnDestroy(): void { + this.observer?.disconnect(); + } + + ngAfterViewInit(): void { + this.observer = new MutationObserver(mutations => { + this.transformOrigin = this.elementRef.nativeElement.style.transformOrigin; + if (!this.transformOrigin) { + return; + } + + const [x, y] = this.transformOrigin.split(" "); + if (x === 'center') { + this.what = 'Y' + if (y === 'top') { + this.value = '-8px' + } else { + this.value = '8px' + } + } else { + this.what = 'X' + if (x === 'left') { + this.value = '-8px' + } else { + this.value = '8px' + } + } + + this._appAnimate = true; + this.cdr.detectChanges(); + }); + this.observer.observe(this.elementRef.nativeElement, { attributes: true, attributeFilter: ['style'] }) + } +} + diff --git a/desktop/angular/projects/safing/ui/src/lib/tooltip/tooltip.module.ts b/desktop/angular/projects/safing/ui/src/lib/tooltip/tooltip.module.ts new file mode 100644 index 00000000..49bd0a14 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tooltip/tooltip.module.ts @@ -0,0 +1,23 @@ +import { OverlayModule } from "@angular/cdk/overlay"; +import { PortalModule } from "@angular/cdk/portal"; +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { SfngTooltipDirective } from "./tooltip"; +import { SfngTooltipComponent } from "./tooltip-component"; + +@NgModule({ + imports: [ + PortalModule, + OverlayModule, + CommonModule, + ], + declarations: [ + SfngTooltipDirective, + SfngTooltipComponent + ], + exports: [ + SfngTooltipDirective + ] +}) +export class SfngTooltipModule { } + diff --git a/desktop/angular/projects/safing/ui/src/lib/tooltip/tooltip.ts b/desktop/angular/projects/safing/ui/src/lib/tooltip/tooltip.ts new file mode 100644 index 00000000..032e6bec --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/tooltip/tooltip.ts @@ -0,0 +1,244 @@ +/* eslint-disable @angular-eslint/no-input-rename */ +import { coerceNumberProperty } from "@angular/cdk/coercion"; +import { ConnectedPosition, Overlay, OverlayRef, PositionStrategy } from "@angular/cdk/overlay"; +import { ComponentPortal } from "@angular/cdk/portal"; +import { ComponentRef, Directive, ElementRef, HostListener, Injector, Input, isDevMode, OnChanges, OnDestroy, OnInit, TemplateRef } from "@angular/core"; +import { Subject } from "rxjs"; +import { SfngTooltipComponent, SFNG_TOOLTIP_CONTENT, SFNG_TOOLTIP_OVERLAY } from "./tooltip-component"; + +/** The allowed tooltip positions. */ +export type SfngTooltipPosition = 'left' | 'right' | 'bottom' | 'top'; + +@Directive({ + selector: '[sfng-tooltip],[snfgTooltip]', +}) +export class SfngTooltipDirective implements OnInit, OnDestroy, OnChanges { + /** Used to control the visibility of the tooltip */ + private attach$ = new Subject(); + + /** Holds a reference to the tooltip overlay */ + private tooltipRef: ComponentRef | null = null; + + /** + * A reference to a timeout created by setTimeout used to debounce + * displaying the tooltip + */ + private debouncer: any | null = null; + + constructor( + private overlay: Overlay, + private injector: Injector, + private originRef: ElementRef, + ) { } + + @HostListener('mouseenter') + show(delay = this.delay) { + if (this.debouncer !== null) { + clearTimeout(this.debouncer); + } + + this.debouncer = setTimeout(() => { + this.debouncer = null; + this.attach$.next(true); + }, delay); + } + + @HostListener('mouseleave') + hide(delay = this.delay / 2) { + // if we're currently debouncing a "show" than + // we should clear that out to avoid re-attaching + // the tooltip right after we disposed it. + if (this.debouncer !== null) { + clearTimeout(this.debouncer); + this.debouncer = null; + } + + this.debouncer = setTimeout(() => { + this.attach$.next(false); + this.debouncer = null; + }, delay); + } + + /** Debounce delay before showing the tooltip */ + @Input('sfngTooltipDelay') + set delay(v: any) { + this._delay = coerceNumberProperty(v); + } + get delay() { return this._delay } + private _delay = 500; + + /** An additional offset between the tooltip overlay and the origin centers */ + @Input('sfngTooltipOffset') + set offset(v: any) { + this._offset = coerceNumberProperty(v); + } + private _offset: number | null = 8; + + /** The actual content that should be displayed in the tooltip overlay. */ + @Input('sfngTooltip') + @Input('sfng-tooltip') + tooltipContent: string | TemplateRef | null = null; + + @Input('snfgTooltipPosition') + position: ConnectedPosition | SfngTooltipPosition | (SfngTooltipPosition | ConnectedPosition)[] | 'any' = 'any'; + + ngOnInit() { + this.attach$ + .subscribe(attach => { + if (attach) { + this.createTooltip(); + return; + } + if (!!this.tooltipRef) { + this.tooltipRef.instance.dispose(); + this.tooltipRef = null; + } + }) + } + + ngOnDestroy(): void { + this.attach$.next(false); + this.attach$.complete(); + } + + ngOnChanges(): void { + // if the tooltip content has be set to null and we're still + // showing the tooltip we treat that as an attempt to hide. + if (this.tooltipContent === null && !!this.tooltipRef) { + this.hide(); + } + } + + /** Creates the actual tooltip overlay */ + private createTooltip() { + // there's nothing to do if the tooltip is still active. + if (!!this.tooltipRef) { + return; + } + + // support disabling the tooltip by passing "null" for + // the content. + if (this.tooltipContent === null) { + return; + } + + const position = this.buildPositionStrategy(); + + const overlayRef = this.overlay.create({ + positionStrategy: position, + scrollStrategy: this.overlay.scrollStrategies.close(), + disposeOnNavigation: true, + }); + + // make sure we close the tooltip if the user clicks on our + // originRef. + overlayRef.outsidePointerEvents() + .subscribe(() => this.hide()); + + overlayRef.attachments() + .subscribe(() => { + if (!overlayRef) { + return + } + overlayRef.updateSize({}); + overlayRef.updatePosition(); + }) + + // create a component portal for the tooltip component + // and attach it to our newly created overlay. + const portal = this.getOverlayPortal(overlayRef); + this.tooltipRef = overlayRef.attach(portal); + } + + private getOverlayPortal(ref: OverlayRef): ComponentPortal { + const inj = Injector.create({ + providers: [ + { provide: SFNG_TOOLTIP_CONTENT, useValue: this.tooltipContent }, + { provide: SFNG_TOOLTIP_OVERLAY, useValue: ref }, + ], + parent: this.injector, + name: 'SfngTooltipDirective' + }) + + const portal = new ComponentPortal( + SfngTooltipComponent, + undefined, + inj + ) + + return portal; + } + + /** Builds a FlexibleConnectedPositionStrategy for the tooltip overlay */ + private buildPositionStrategy(): PositionStrategy { + let pos = this.position; + if (pos === 'any') { + pos = ['top', 'bottom', 'right', 'left'] + } else if (!Array.isArray(pos)) { + pos = [pos]; + } + + let allowedPositions: ConnectedPosition[] = + pos.map(p => { + if (typeof p === 'string') { + return this.getAllowedConnectedPosition(p); + } + // this is already a ConnectedPosition + return p + }); + + let position = this.overlay.position() + .flexibleConnectedTo(this.originRef) + .withFlexibleDimensions(true) + .withPush(true) + .withPositions(allowedPositions) + .withGrowAfterOpen(true) + .withTransformOriginOn('.sfng-tooltip-instance') + + return position; + } + + private getAllowedConnectedPosition(type: SfngTooltipPosition): ConnectedPosition { + switch (type) { + case 'left': + return { + originX: 'start', + originY: 'center', + overlayX: 'end', + overlayY: 'center', + offsetX: - (this._offset || 0), + } + case 'right': + return { + originX: 'end', + originY: 'center', + overlayX: 'start', + overlayY: 'center', + offsetX: (this._offset || 0), + } + case 'top': + return { + originX: 'center', + originY: 'top', + overlayX: 'center', + overlayY: 'bottom', + offsetY: - (this._offset || 0), + } + case 'bottom': + return { + originX: 'center', + originY: 'bottom', + overlayX: 'center', + overlayY: 'top', + offsetY: (this._offset || 0), + } + default: + if (isDevMode()) { + throw new Error(`invalid value for SfngTooltipPosition: ${type}`) + } + // fallback to "right" + return this.getAllowedConnectedPosition('right') + } + } +} + diff --git a/desktop/angular/projects/safing/ui/src/lib/ui.module.ts b/desktop/angular/projects/safing/ui/src/lib/ui.module.ts new file mode 100644 index 00000000..e1f772ae --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/lib/ui.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { SfngAccordionModule } from './accordion'; + + +@NgModule({ + exports: [ + SfngAccordionModule + ] +}) +export class UiModule { } diff --git a/desktop/angular/projects/safing/ui/src/public-api.ts b/desktop/angular/projects/safing/ui/src/public-api.ts new file mode 100644 index 00000000..e07d6adf --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/public-api.ts @@ -0,0 +1,16 @@ +/* + * Public API Surface of ui + */ + +export * from './lib/accordion'; +export * from './lib/dialog'; +export * from './lib/dropdown'; +export * from './lib/overlay-stepper'; +export * from './lib/pagination'; +export * from './lib/select'; +export * from './lib/tabs'; +export * from './lib/tipup'; +export * from './lib/toggle-switch'; +export * from './lib/tooltip'; +export * from './lib/ui.module'; + diff --git a/desktop/angular/projects/safing/ui/src/test.ts b/desktop/angular/projects/safing/ui/src/test.ts new file mode 100644 index 00000000..ceee7e40 --- /dev/null +++ b/desktop/angular/projects/safing/ui/src/test.ts @@ -0,0 +1,16 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; +import 'zone.js'; +import 'zone.js/testing'; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + { teardown: { destroyAfterEach: true } }, +); diff --git a/desktop/angular/projects/safing/ui/theming.scss b/desktop/angular/projects/safing/ui/theming.scss new file mode 100644 index 00000000..9c5bb3c9 --- /dev/null +++ b/desktop/angular/projects/safing/ui/theming.scss @@ -0,0 +1,8 @@ +@import "./src/lib/select/select"; +@import "./src/lib/dialog/dialog"; +@import "./src/lib/pagination/pagination"; +@import "./src/lib/tabs/tab-group"; +@import "./src/lib/tipup/tipup"; +@import "./src/lib/tooltip/tooltip-component"; +@import "./src/lib/toggle-switch/toggle-switch"; +@import "./src/lib/dialog/confirm.dialog"; diff --git a/desktop/angular/projects/safing/ui/tsconfig.lib.json b/desktop/angular/projects/safing/ui/tsconfig.lib.json new file mode 100644 index 00000000..703cd4fd --- /dev/null +++ b/desktop/angular/projects/safing/ui/tsconfig.lib.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "../../../out-tsc/lib", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [], + "lib": [ + "dom", + "es2018" + ] + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/desktop/angular/projects/safing/ui/tsconfig.lib.prod.json b/desktop/angular/projects/safing/ui/tsconfig.lib.prod.json new file mode 100644 index 00000000..71b135f6 --- /dev/null +++ b/desktop/angular/projects/safing/ui/tsconfig.lib.prod.json @@ -0,0 +1,7 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, +} diff --git a/desktop/angular/projects/safing/ui/tsconfig.spec.json b/desktop/angular/projects/safing/ui/tsconfig.spec.json new file mode 100644 index 00000000..85392ee8 --- /dev/null +++ b/desktop/angular/projects/safing/ui/tsconfig.spec.json @@ -0,0 +1,17 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "../../../out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/desktop/angular/projects/tauri-builtin/src/app/app.component.html b/desktop/angular/projects/tauri-builtin/src/app/app.component.html new file mode 100644 index 00000000..c8897e1e --- /dev/null +++ b/desktop/angular/projects/tauri-builtin/src/app/app.component.html @@ -0,0 +1,105 @@ +
+ + + +
+

Safing

+

+ Portmaster +

+
+
+ +
+ + + Connecting to System Service ... + + + + Connecting to System Service ... + + + + + Portmaster System Service is not running: + + + + + + + + + + + + Failed to find Portmaster System Service. +
+ Please reinstall the application. +
+ + +
+ + + + + + + + Your System Service Manager is not supported. Please make sure Portmaster is running. + + + + + + + + + + Your System Service Manager is not supported. Please make sure Portmaster is running. + + + + Unknown error: {{ status }} +
\ No newline at end of file diff --git a/desktop/angular/projects/tauri-builtin/src/app/app.component.ts b/desktop/angular/projects/tauri-builtin/src/app/app.component.ts new file mode 100644 index 00000000..b39cd515 --- /dev/null +++ b/desktop/angular/projects/tauri-builtin/src/app/app.component.ts @@ -0,0 +1,52 @@ +import { OnInit, Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ServiceManagerStatus, TauriIntegrationService } from 'src/app/integration/taur-app'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [CommonModule], + templateUrl: './app.component.html', + styles: [ + ` + :host { + @apply block w-screen h-screen bg-background; + } + + #logo svg { + @apply absolute w-20; + } + `, + ], +}) +export class AppComponent implements OnInit { + private tauri = inject(TauriIntegrationService); + + status: ServiceManagerStatus | string | null = null; + + getHelp() { + this.tauri.openExternal("https://wiki.safing.io/en/Portmaster/App") + } + + startService() { + this.tauri.startService() + .then(() => this.getStatus()) + .catch(err => { + this.status = err.error; + }); + } + + getStatus() { + this.tauri.getServiceManagerStatus() + .then(result => { + this.status = result; + }) + .catch(err => { + this.status = err.error; + }) + } + + ngOnInit() { + this.getStatus(); + } +} diff --git a/desktop/angular/projects/tauri-builtin/src/app/app.config.ts b/desktop/angular/projects/tauri-builtin/src/app/app.config.ts new file mode 100644 index 00000000..2b4aa00c --- /dev/null +++ b/desktop/angular/projects/tauri-builtin/src/app/app.config.ts @@ -0,0 +1,12 @@ +import { ApplicationConfig } from '@angular/core'; +import { TauriIntegrationService } from 'src/app/integration/taur-app'; + +export const appConfig: ApplicationConfig = { + providers: [ + { + provide: TauriIntegrationService, + useClass: TauriIntegrationService, + deps: [] + }, + ], +}; diff --git a/desktop/angular/projects/tauri-builtin/src/assets b/desktop/angular/projects/tauri-builtin/src/assets new file mode 120000 index 00000000..2978ef39 --- /dev/null +++ b/desktop/angular/projects/tauri-builtin/src/assets @@ -0,0 +1 @@ +../../../assets \ No newline at end of file diff --git a/desktop/angular/projects/tauri-builtin/src/favicon.ico b/desktop/angular/projects/tauri-builtin/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..997406ad22c29aae95893fb3d666c30258a09537 GIT binary patch literal 948 zcmV;l155mgP)CBYU7IjCFmI-B}4sMJt3^s9NVg!P0 z6hDQy(L`XWMkB@zOLgN$4KYz;j0zZxq9KKdpZE#5@k0crP^5f9KO};h)ZDQ%ybhht z%t9#h|nu0K(bJ ztIkhEr!*UyrZWQ1k2+YkGqDi8Z<|mIN&$kzpKl{cNP=OQzXHz>vn+c)F)zO|Bou>E z2|-d_=qY#Y+yOu1a}XI?cU}%04)zz%anD(XZC{#~WreV!a$7k2Ug`?&CUEc0EtrkZ zL49MB)h!_K{H(*l_93D5tO0;BUnvYlo+;yss%n^&qjt6fZOa+}+FDO(~2>G z2dx@=JZ?DHP^;b7*Y1as5^uphBsh*s*z&MBd?e@I>-9kU>63PjP&^#5YTOb&x^6Cf z?674rmSHB5Fk!{Gv7rv!?qX#ei_L(XtwVqLX3L}$MI|kJ*w(rhx~tc&L&xP#?cQow zX_|gx$wMr3pRZIIr_;;O|8fAjd;1`nOeu5K(pCu7>^3E&D2OBBq?sYa(%S?GwG&_0-s%_v$L@R!5H_fc)lOb9ZoOO#p`Nn`KU z3LTTBtjwo`7(HA6 z7gmO$yTR!5L>Bsg!X8616{JUngg_@&85%>W=mChTR;x4`P=?PJ~oPuy5 zU-L`C@_!34D21{fD~Y8NVnR3t;aqZI3fIhmgmx}$oc-dKDC6Ap$Gy>a!`A*x2L1v0 WcZ@i?LyX}70000 + + + + TauriBuiltin + + + + + + + + diff --git a/desktop/angular/projects/tauri-builtin/src/main.ts b/desktop/angular/projects/tauri-builtin/src/main.ts new file mode 100644 index 00000000..35b00f34 --- /dev/null +++ b/desktop/angular/projects/tauri-builtin/src/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, appConfig) + .catch((err) => console.error(err)); diff --git a/desktop/angular/projects/tauri-builtin/src/styles.scss b/desktop/angular/projects/tauri-builtin/src/styles.scss new file mode 100644 index 00000000..66a2c66c --- /dev/null +++ b/desktop/angular/projects/tauri-builtin/src/styles.scss @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@import "safing/ui/theming"; + +/** foboar **/ diff --git a/desktop/angular/projects/tauri-builtin/tsconfig.app.json b/desktop/angular/projects/tauri-builtin/tsconfig.app.json new file mode 100644 index 00000000..f12c6239 --- /dev/null +++ b/desktop/angular/projects/tauri-builtin/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts", "../../src/electron-app.d.ts"] +} diff --git a/desktop/angular/proxy.json b/desktop/angular/proxy.json new file mode 100644 index 00000000..c60a2a4c --- /dev/null +++ b/desktop/angular/proxy.json @@ -0,0 +1,6 @@ +{ + "/api": { + "target": "http://localhost:817/", + "secure": false + } +} diff --git a/desktop/angular/src/app/app-routing.module.ts b/desktop/angular/src/app/app-routing.module.ts new file mode 100644 index 00000000..2324ae8b --- /dev/null +++ b/desktop/angular/src/app/app-routing.module.ts @@ -0,0 +1,68 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AppViewComponent } from './pages/app-view'; +import { DashboardPageComponent } from './pages/dashboard/dashboard.component'; +import { MonitorPageComponent } from './pages/monitor'; +import { SettingsComponent } from './pages/settings/settings'; +import { SpnPageComponent } from './pages/spn'; +import { SupportPageComponent } from './pages/support'; +import { SupportFormComponent } from './pages/support/form'; + +const routes: Routes = [ + { + path: '', + pathMatch: 'full', + redirectTo: 'dashboard', + }, + { + path: 'settings', + component: SettingsComponent, + }, + { + path: 'app', + pathMatch: 'full', + redirectTo: 'app/overview', + }, + { + path: 'app/overview', + component: AppViewComponent, + }, + { + path: 'app/:source/:id', + component: AppViewComponent, + }, + { + path: 'monitor', + component: MonitorPageComponent, + }, + { + path: 'monitor/profile/:source/:profile', + redirectTo: 'monitor', + }, + { + path: 'support', + component: SupportPageComponent, + }, + { + path: 'support/:id', + component: SupportFormComponent, + }, + { + path: 'spn', + component: SpnPageComponent, + }, + { + path: '**', + redirectTo: 'dashboard' + }, + { + path: 'dashboard', + component: DashboardPageComponent + } +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes, { anchorScrolling: 'enabled' })], + exports: [RouterModule] +}) +export class AppRoutingModule { } diff --git a/desktop/angular/src/app/app.component.html b/desktop/angular/src/app/app.component.html new file mode 100644 index 00000000..401b4a49 --- /dev/null +++ b/desktop/angular/src/app/app.component.html @@ -0,0 +1,53 @@ + + + +
+ + + + +
+ +
+ +
+
+ +

{{overlayText}}

+

...

+
+
diff --git a/desktop/angular/src/app/app.component.scss b/desktop/angular/src/app/app.component.scss new file mode 100644 index 00000000..52cb3a92 --- /dev/null +++ b/desktop/angular/src/app/app.component.scss @@ -0,0 +1,114 @@ +:host { + display: flex; + @apply bg-background; + @apply h-screen overflow-hidden; + + &>* { + flex-shrink: 0; + } +} + +app-navigation, +app-side-dash { + @apply border-r; + @apply border-cards-tertiary; + @apply bg-background; +} + +app-navigation { + @apply w-16; +} + +div.main { + flex-grow: 1; + flex-shrink: 1; + + display: flex; + flex-direction: column; + align-items: center; + @apply bg-background; + height: 100vh; + overflow: hidden; +} + +app-debug { + @apply border-l; + @apply border-cards-tertiary; + @apply bg-background; + + width: 30vw; + height: 100vh; + min-width: 350px; + top: 0px; + position: sticky; +} + +.loading { + z-index: 100; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + backdrop-filter: blur(10px); + background-color: rgba(#222222, 0.35); + + .message { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + flex-direction: column; + } + + svg { + width: 100%; + position: absolute; + top: 0; + left: 0; + } + + div.logo { + opacity: 0.8; + position: relative; + width: 10vh; + height: 10vh; + @apply mt-4; + } + + .spin { + animation-name: spin; + animation-duration: 3500ms; + animation-iteration-count: infinite; + animation-timing-function: linear; + } + + .reverse { + animation-name: spin-reverse; + } +} + + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +@keyframes spin-reverse { + 0% { + transform: rotate(360deg); + } + + 100% { + transform: rotate(0deg); + } +} diff --git a/desktop/angular/src/app/app.component.spec.ts b/desktop/angular/src/app/app.component.spec.ts new file mode 100644 index 00000000..200892c0 --- /dev/null +++ b/desktop/angular/src/app/app.component.spec.ts @@ -0,0 +1,28 @@ +import { TestBed, waitForAsync } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule + ], + declarations: [ + AppComponent + ], + }).compileComponents(); + })); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have as title 'portmaster'`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('portmaster'); + }); +}); diff --git a/desktop/angular/src/app/app.component.ts b/desktop/angular/src/app/app.component.ts new file mode 100644 index 00000000..1ea813cd --- /dev/null +++ b/desktop/angular/src/app/app.component.ts @@ -0,0 +1,234 @@ +import { Overlay } from '@angular/cdk/overlay'; +import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, HostListener, Inject, NgZone, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { Params, Router } from '@angular/router'; +import { PortapiService } from '@safing/portmaster-api'; +import { OverlayStepper, SfngDialogService, StepperRef } from '@safing/ui'; +import { BehaviorSubject, merge, Subject } from 'rxjs'; +import { debounceTime, filter, mergeMap, skip, startWith, take } from 'rxjs/operators'; +import { IntroModule } from './intro'; +import { NotificationsService, UIStateService } from './services'; +import { ActionIndicatorService } from './shared/action-indicator'; +import { fadeInAnimation, fadeOutAnimation } from './shared/animations'; +import { ExitService } from './shared/exit-screen'; +import { SfngNetquerySearchOverlayComponent } from './shared/netquery/search-overlay'; +import { INTEGRATION_SERVICE, IntegrationService } from './integration'; +import { TauriIntegrationService } from './integration/taur-app'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], + animations: [ + fadeInAnimation, + fadeOutAnimation, + ] +}) +export class AppComponent implements OnInit, AfterViewInit { + readonly connected = this.portapi.connected$.pipe( + debounceTime(250), + startWith(false) + ); + title = 'portmaster'; + + /** The current status of the side dash as emitted by the navigation component */ + sideDashStatus: 'collapsed' | 'expanded' = 'expanded'; + + /** Whether or not the side-dash is in overlay mode */ + sideDashOverlay = false; + + /** The MQL to watch for screen size changes. */ + private mql!: MediaQueryList; + + /** Emits when the side-dash is opened or closed in non-overlay mode */ + private sideDashOpen = new BehaviorSubject(false); + + /** Used to emit when the window size changed */ + windowResizeChange = new Subject(); + + get sideDashOpen$() { return this.sideDashOpen.asObservable() } + + get showOverlay$() { return this.exitService.showOverlay$ } + + get onContentSizeChange$() { + return merge( + this.windowResizeChange, + this.sideDashOpen$ + ) + .pipe( + startWith(undefined), + debounceTime(100), + ) + } + + @ViewChild('mainContent', { read: ElementRef, static: true }) + mainContent!: ElementRef; + + @HostListener('window:resize') + onWindowResize() { + this.windowResizeChange.next(); + } + + @HostListener('document:keydown', ['$event']) + onKeyDown(event: KeyboardEvent) { + if (event.key === ' ' && event.ctrlKey) { + this.dialog.create( + SfngNetquerySearchOverlayComponent, + { + positionStrategy: this.overlay + .position() + .global() + .centerHorizontally() + .top('1rem'), + backdrop: 'light', + autoclose: true, + } + ) + return; + } + } + + constructor( + public ngZone: NgZone, + public portapi: PortapiService, + public changeDetectorRef: ChangeDetectorRef, + private router: Router, + private exitService: ExitService, + private overlayStepper: OverlayStepper, + private dialog: SfngDialogService, + private overlay: Overlay, + private stateService: UIStateService, + private renderer2: Renderer2, + @Inject(INTEGRATION_SERVICE) private integration: IntegrationService, + ) { + (window as any).portapi = portapi; + } + + onSideDashChange(state: 'expanded' | 'collapsed' | 'force-overlay') { + if (state === 'force-overlay') { + state = 'expanded'; + if (!this.sideDashOverlay) { + this.sideDashOverlay = true; + } + } else { + this.sideDashOverlay = this.mql.matches; + } + + this.sideDashStatus = state; + + if (!this.sideDashOverlay) { + this.sideDashOpen.next(this.sideDashStatus === 'expanded') + } + } + + ngOnInit() { + // default breakpoints used by tailwindcss + const minContentWithBp = [ + 640, // sfng-sm: + 768, // sfng-md: + 1024, // sfng-lg: + 1280, // sfng-xl: + 1536 // sfng-2xl: + ] + + // prepare our breakpoint listeners and add the classes to our main element + merge( + this.windowResizeChange, + this.sideDashOpen$ + ) + .pipe( + startWith(undefined), + debounceTime(100), + ) + .subscribe(() => { + const rect = (this.mainContent.nativeElement as HTMLElement).getBoundingClientRect(); + + minContentWithBp.forEach((bp, idx) => { + if (rect.width >= bp) { + this.renderer2.addClass(this.mainContent.nativeElement, `min-width-${bp}px`) + } else { + this.renderer2.removeClass(this.mainContent.nativeElement, `min-width-${bp}px`) + } + }) + + this.changeDetectorRef.markForCheck(); + }) + + // force a reload of the current route if we reconnected to + // portmaster. This ensures we'll refresh any data that's currently + // displayed. + this.connected + .pipe( + filter(connected => !!connected), + skip(1), + ) + .subscribe(async () => { + const location = new URL(window.location.toString()); + + const params: Params = {} + location.searchParams.forEach((value, key) => { + params[key] = [ + ...(params[key] || []), + value, + ] + }) + + await this.router.navigateByUrl('/', { skipLocationChange: true }) + this.router.navigate([location.pathname], { + queryParams: params, + }); + }) + + this.stateService.uiState() + .pipe(take(1)) + .subscribe(state => { + if (!state.introScreenFinished) { + this.showIntro(); + } + }) + + this.mql = window.matchMedia('(max-width: 1200px)'); + this.sideDashOverlay = this.mql.matches; + + this.mql.addEventListener('change', () => { + this.sideDashOverlay = this.mql.matches; + + if (!this.sideDashOverlay) { + this.sideDashOpen.next(this.sideDashStatus === 'expanded') + } + }) + } + + ngAfterViewInit(): void { + this.sideDashOpen.next(this.sideDashStatus !== 'collapsed') + + if (this.integration instanceof TauriIntegrationService) { + let tauri = this.integration; + + tauri.shouldShow() + .then(show => { + console.log("should open window: ", show) + if (show) { + tauri.openApp(); + } + }); + } + } + + showIntro(): StepperRef { + const stepperRef = this.overlayStepper.create(IntroModule.Stepper) + + stepperRef.onFinish.subscribe(() => { + this.stateService.uiState() + .pipe( + take(1), + mergeMap(state => this.stateService.saveState({ + ...state, + introScreenFinished: true + })) + ) + .subscribe(); + }) + + return stepperRef; + } +} diff --git a/desktop/angular/src/app/app.module.ts b/desktop/angular/src/app/app.module.ts new file mode 100644 index 00000000..c90aaec5 --- /dev/null +++ b/desktop/angular/src/app/app.module.ts @@ -0,0 +1,240 @@ +import { DragDropModule } from '@angular/cdk/drag-drop'; +import { OverlayModule } from '@angular/cdk/overlay'; +import { PortalModule } from '@angular/cdk/portal'; +import { ScrollingModule } from '@angular/cdk/scrolling'; +import { CdkTableModule } from '@angular/cdk/table'; +import { CommonModule, registerLocaleData } from '@angular/common'; +import { HttpClientModule } from '@angular/common/http'; + +import { APP_INITIALIZER, LOCALE_ID, NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { FaIconLibrary, FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { faGithub } from '@fortawesome/free-brands-svg-icons'; +import { far } from '@fortawesome/free-regular-svg-icons'; +import { fas } from '@fortawesome/free-solid-svg-icons'; +import { ConfigService, PortmasterAPIModule, StringSetting, getActualValue } from '@safing/portmaster-api'; +import { OverlayStepperModule, SfngAccordionModule, SfngDialogModule, SfngDropDownModule, SfngPaginationModule, SfngSelectModule, SfngTipUpModule, SfngToggleSwitchModule, SfngTooltipModule, TabModule, UiModule } from '@safing/ui'; +import MyYamlFile from 'js-yaml-loader!../i18n/helptexts.yaml'; +import * as i18n from 'ng-zorro-antd/i18n'; +import { MarkdownModule } from 'ngx-markdown'; +import { firstValueFrom } from 'rxjs'; +import { environment } from 'src/environments/environment'; +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { IntroModule } from './intro'; +import { NavigationComponent } from './layout/navigation/navigation'; +import { SideDashComponent } from './layout/side-dash/side-dash'; +import { AppOverviewComponent, AppViewComponent, QuickSettingInternetButtonComponent } from './pages/app-view'; +import { QsHistoryComponent } from './pages/app-view/qs-history/qs-history.component'; +import { QuickSettingSelectExitButtonComponent } from './pages/app-view/qs-select-exit/qs-select-exit'; +import { QuickSettingUseSPNButtonComponent } from './pages/app-view/qs-use-spn/qs-use-spn'; +import { DashboardPageComponent } from './pages/dashboard/dashboard.component'; +import { FeatureCardComponent } from './pages/dashboard/feature-card/feature-card.component'; +import { MonitorPageComponent } from './pages/monitor'; +import { SettingsComponent } from './pages/settings/settings'; +import { SPNModule } from './pages/spn/spn.module'; +import { SupportPageComponent } from './pages/support'; +import { SupportFormComponent } from './pages/support/form'; +import { NotificationsService } from './services'; +import { ActionIndicatorModule } from './shared/action-indicator'; +import { SfngAppIconModule } from './shared/app-icon'; +import { ConfigModule } from './shared/config'; +import { CountIndicatorModule } from './shared/count-indicator'; +import { CountryFlagModule } from './shared/country-flag'; +import { EditProfileDialog } from './shared/edit-profile-dialog'; +import { ExitScreenComponent } from './shared/exit-screen/exit-screen'; +import { ExpertiseModule } from './shared/expertise/expertise.module'; +import { ExternalLinkDirective } from './shared/external-link.directive'; +import { FeatureScoutComponent } from './shared/feature-scout'; +import { SfngFocusModule } from './shared/focus'; +import { FuzzySearchPipe } from './shared/fuzzySearch'; +import { LoadingComponent } from './shared/loading'; +import { SfngMenuModule } from './shared/menu'; +import { SfngMultiSwitchModule } from './shared/multi-switch'; +import { NetqueryModule } from './shared/netquery'; +import { NetworkScoutComponent } from './shared/network-scout'; +import { NotificationListComponent } from './shared/notification-list/notification-list.component'; +import { NotificationComponent } from './shared/notification/notification'; +import { CommonPipesModule } from './shared/pipes'; +import { ProcessDetailsDialogComponent } from './shared/process-details-dialog'; +import { PromptListComponent } from './shared/prompt-list/prompt-list.component'; +import { SecurityLockComponent } from './shared/security-lock'; +import { SPNAccountDetailsComponent } from './shared/spn-account-details'; +import { SPNLoginComponent } from './shared/spn-login'; +import { SPNStatusComponent } from './shared/spn-status'; +import { PilotWidgetComponent } from './shared/status-pilot'; +import { PlaceholderComponent } from './shared/text-placeholder'; +import { DashboardWidgetComponent } from './pages/dashboard/dashboard-widget/dashboard-widget.component'; +import { MergeProfileDialogComponent } from './pages/app-view/merge-profile-dialog/merge-profile-dialog.component'; +import { AppInsightsComponent } from './pages/app-view/app-insights/app-insights.component'; +import { INTEGRATION_SERVICE, integrationServiceFactory } from './integration'; +import { SupportProgressDialogComponent } from './pages/support/progress-dialog'; + +function loadAndSetLocaleInitializer(configService: ConfigService) { + return async function () { + let angularLocaleID = 'en-GB'; + let nzLocaleID: string = 'en_GB'; + + try { + const setting = await firstValueFrom(configService.get("core/locale")) + + const currentValue = getActualValue(setting as StringSetting); + switch (currentValue) { + case 'en-US': + angularLocaleID = 'en-US' + nzLocaleID = 'en_US' + break; + case 'en-GB': + angularLocaleID = 'en-GB' + nzLocaleID = 'en_GB' + break; + + default: + console.error(`Unsupported locale value: ${currentValue}, defaulting to en-GB`) + } + } catch (err) { + console.error(`failed to get locale setting, using default en-GB:`, err) + } + + try { + // Get name of module. + let localeModuleID = angularLocaleID; + if (localeModuleID == "en-US") { + localeModuleID = "en"; + } + + /* webpackInclude: /(en|en-GB)\.mjs$/ */ + /* webpackChunkName: "./l10n-base/[request]"*/ + await import(`../../node_modules/@angular/common/locales/${localeModuleID}.mjs`) + .then(locale => { + registerLocaleData(locale.default) + + localeConfig.localeId = angularLocaleID; + localeConfig.nzLocale = (i18n as any)[nzLocaleID]; + }) + } catch (err) { + console.error(`failed to load locale module for ${angularLocaleID}:`, err) + } + } +} + +const localeConfig = { + nzLocale: i18n.en_GB, + localeId: 'en-GB' +} + +@NgModule({ + declarations: [ + AppComponent, + NotificationComponent, + SettingsComponent, + MonitorPageComponent, + SideDashComponent, + NavigationComponent, + PilotWidgetComponent, + NotificationListComponent, + PromptListComponent, + FuzzySearchPipe, + AppViewComponent, + QuickSettingInternetButtonComponent, + QuickSettingUseSPNButtonComponent, + QuickSettingSelectExitButtonComponent, + AppOverviewComponent, + PlaceholderComponent, + LoadingComponent, + ExternalLinkDirective, + ExitScreenComponent, + SupportPageComponent, + SupportFormComponent, + SecurityLockComponent, + SPNStatusComponent, + FeatureScoutComponent, + SPNLoginComponent, + SPNAccountDetailsComponent, + NetworkScoutComponent, + EditProfileDialog, + ProcessDetailsDialogComponent, + QsHistoryComponent, + DashboardPageComponent, + DashboardWidgetComponent, + FeatureCardComponent, + MergeProfileDialogComponent, + AppInsightsComponent, + SupportProgressDialogComponent + ], + imports: [ + BrowserModule, + CommonModule, + BrowserAnimationsModule, + FormsModule, + ReactiveFormsModule, + AppRoutingModule, + FontAwesomeModule, + OverlayModule, + PortalModule, + CdkTableModule, + DragDropModule, + HttpClientModule, + MarkdownModule.forRoot(), + ScrollingModule, + SfngAccordionModule, + TabModule, + SfngTipUpModule.forRoot(MyYamlFile, NotificationsService), + SfngTooltipModule, + ActionIndicatorModule, + SfngDialogModule, + OverlayStepperModule, + IntroModule, + SfngDropDownModule, + SfngSelectModule, + SfngMultiSwitchModule, + SfngMenuModule, + SfngFocusModule, + SfngToggleSwitchModule, + SfngPaginationModule, + SfngAppIconModule, + ExpertiseModule, + ConfigModule, + CountryFlagModule, + CountIndicatorModule, + NetqueryModule, + CommonPipesModule, + UiModule, + SPNModule, + PortmasterAPIModule.forRoot({ + httpAPI: environment.httpAPI, + websocketAPI: environment.portAPI, + }), + ], + bootstrap: [AppComponent], + providers: [ + { + provide: APP_INITIALIZER, useFactory: loadAndSetLocaleInitializer, deps: [ConfigService], multi: true + }, + { + provide: i18n.NZ_I18N, useFactory: () => { + console.log("nz-locale is set to", localeConfig.nzLocale) + return localeConfig.nzLocale + } + }, + { + provide: LOCALE_ID, useFactory: () => { + console.log("locale-id is set to", localeConfig.localeId) + return localeConfig.localeId + } + }, + { + provide: INTEGRATION_SERVICE, + useFactory: integrationServiceFactory + } + ] +}) +export class AppModule { + constructor(library: FaIconLibrary) { + library.addIconPacks(fas, far); + library.addIcons(faGithub) + } +} + diff --git a/desktop/angular/src/app/integration/browser.ts b/desktop/angular/src/app/integration/browser.ts new file mode 100644 index 00000000..82504c3b --- /dev/null +++ b/desktop/angular/src/app/integration/browser.ts @@ -0,0 +1,41 @@ +import { AppInfo, IntegrationService, ProcessInfo } from "./integration"; + +export class BrowserIntegrationService implements IntegrationService { + writeToClipboard(text: string): Promise { + if (!!navigator.clipboard) { + return navigator.clipboard.writeText(text); + } + + return Promise.reject(new Error(`Clipboard API not supported`)) + } + + openExternal(pathOrUrl: string): Promise { + window.open(pathOrUrl, '_blank') + + return Promise.resolve(); + } + + getInstallDir(): Promise { + return Promise.reject('Not supported in browser') + } + + getAppIcon(_: ProcessInfo): Promise { + return Promise.reject('Not supported in browser') + } + + getAppInfo(_: ProcessInfo): Promise { + return Promise.reject('Not supported in browser') + } + + exitApp(): Promise { + window.close(); + + return Promise.resolve(); + } + + onExitRequest(cb: () => void): () => void { + // nothing to do, there + return () => { } + } +} + diff --git a/desktop/angular/src/app/integration/electron.ts b/desktop/angular/src/app/integration/electron.ts new file mode 100644 index 00000000..71b63984 --- /dev/null +++ b/desktop/angular/src/app/integration/electron.ts @@ -0,0 +1,55 @@ +import { BrowserIntegrationService } from "./browser"; +import { AppInfo, ProcessInfo } from "./integration"; + +export class ElectronIntegrationService extends BrowserIntegrationService { + + openExternal(pathOrUrl: string): Promise { + if (!!window.app) { + return window.app.openExternal(pathOrUrl); + } + + return Promise.reject('No electron API available') + } + + getInstallDir(): Promise { + if (!!window.app) { + return window.app.getInstallDir() + } + + return Promise.reject('No electron API available') + } + + getAppIcon(info: ProcessInfo): Promise { + if (!!window.app) { + return window.app.getFileIcon(info.execPath) + } + + return Promise.reject('No electron API available') + } + + getAppInfo(_: ProcessInfo): Promise { + return Promise.reject('Not supported in electron') + } + + exitApp(): Promise { + if (!!window.app) { + window.app.exitApp(); + } + + return Promise.resolve(); + } + + onExitRequest(cb: () => void): () => void { + let listener = (event: MessageEvent) => { + if (event.data === 'on-app-close') { + cb(); + } + } + + window.addEventListener('message', listener); + + return () => { + window.removeEventListener('message', listener) + } + } +} diff --git a/desktop/angular/src/app/integration/factory.ts b/desktop/angular/src/app/integration/factory.ts new file mode 100644 index 00000000..419f3ea9 --- /dev/null +++ b/desktop/angular/src/app/integration/factory.ts @@ -0,0 +1,22 @@ +import { InjectionToken } from "@angular/core"; +import { BrowserIntegrationService } from "./browser"; +import { ElectronIntegrationService } from "./electron"; +import { IntegrationService } from "./integration"; +import { TauriIntegrationService } from "./taur-app"; + +export function integrationServiceFactory(): IntegrationService { + if ('__TAURI__' in window) { + console.log("[app] running under tauri") + return new TauriIntegrationService(); + } + + if ('app' in window) { + console.log("[app] running under electron") + return new ElectronIntegrationService(); + } + + console.log("[app] running in browser") + return new BrowserIntegrationService(); +} + +export const INTEGRATION_SERVICE = new InjectionToken('INTEGRATION_SERVICE'); diff --git a/desktop/angular/src/app/integration/index.ts b/desktop/angular/src/app/integration/index.ts new file mode 100644 index 00000000..de9e8105 --- /dev/null +++ b/desktop/angular/src/app/integration/index.ts @@ -0,0 +1,2 @@ +export * from './integration'; +export * from './factory'; diff --git a/desktop/angular/src/app/integration/integration.ts b/desktop/angular/src/app/integration/integration.ts new file mode 100644 index 00000000..ae426353 --- /dev/null +++ b/desktop/angular/src/app/integration/integration.ts @@ -0,0 +1,41 @@ + +export interface AppInfo { + app_name: string; + comment: string; + icon_dataurl: string; + icon_path: string; +} + +export interface ProcessInfo { + execPath: string; + cmdline: string; + pid: number; + matchingPath: string; +} + +export interface IntegrationService { + /** writeToClipboard copies text to the system clipboard */ + writeToClipboard(text: string): Promise; + + /** openExternal opens a file or URL in an external window */ + openExternal(pathOrUrl: string): Promise; + + /** Gets the path to the portmaster installation directory */ + getInstallDir(): Promise; + + /** Load application information (currently linux only) */ + getAppInfo(info: ProcessInfo): Promise; + + /** Loads the application icon as a dataurl */ + getAppIcon(info: ProcessInfo): Promise; + + /** Closes the application, does not return */ + exitApp(): Promise; + + /** Registers a listener for on-close requests. */ + onExitRequest(cb: () => void): () => void; +} + + + + diff --git a/desktop/angular/src/app/integration/taur-app.ts b/desktop/angular/src/app/integration/taur-app.ts new file mode 100644 index 00000000..f48c3499 --- /dev/null +++ b/desktop/angular/src/app/integration/taur-app.ts @@ -0,0 +1,216 @@ +import { AppInfo, IntegrationService, ProcessInfo } from "./integration"; +import { writeText } from '@tauri-apps/plugin-clipboard-manager'; +import { open } from '@tauri-apps/plugin-shell'; +import { listen, once } from '@tauri-apps/api/event'; +import { invoke } from '@tauri-apps/api/core' +import { getCurrent, Window } from '@tauri-apps/api/window'; + +// Returns a new uuidv4. If crypto.randomUUID is not available it fals back to +// using Math.random(). While this is not as random as it should be it's still +// enough for our use-case here (which is just to generate a random response-id). +function uuid(): string { + if (typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + + // This one is not really random and not RFC compliant but serves enough for fallback + // purposes if the UI is opened in a browser that does not yet support randomUUID + console.warn('Using browser with lacking support for crypto.randomUUID()'); + + return Date.now().toString(36) + Math.random().toString(36).substring(2); +} + +function asyncInvoke(method: string, args: object): Promise { + return new Promise((resolve, reject) => { + const eventId = uuid(); + + once(eventId, (event) => { + if (typeof event.payload === 'object' && 'error' in event.payload) { + reject(event.payload); + return + }; + + resolve(event.payload); + }) + + invoke(method, { + ...args, + responseId: eventId, + }).catch((err: any) => { + console.error("tauri:invoke rejected: ", method, args, err); + reject(err) + }); + }) +} + +export type ServiceManagerStatus = 'Running' | 'Stopped' | 'NotFound' | 'unsupported service manager' | 'unsupported operating system'; + +export class TauriIntegrationService implements IntegrationService { + private withPrompts = false; + + constructor() { + this.shouldHandlePrompts() + .then(result => { + this.withPrompts = result; + }); + + // listen for the portmaster:show event that is emitted + // when tauri want's to tell us that we should make our + // window visible. + listen("portmaster:show", () => { + this.openApp(); + }) + } + + writeToClipboard(text: string): Promise { + return writeText(text); + } + + openExternal(pathOrUrl: string): Promise { + return open(pathOrUrl); + } + + getInstallDir(): Promise { + return Promise.reject("not yet supported in tauri") + } + + getAppInfo(info: ProcessInfo): Promise { + return asyncInvoke("plugin:portmaster|get_app_info", { + ...info, + }) + } + + getAppIcon(info: ProcessInfo): Promise { + return this.getAppInfo(info) + .then(info => info.icon_dataurl) + } + + exitApp(): Promise { + // we have two options here: + // - close(): close the native tauri window and release all resources of it. + // this has the disadvantage that if the user re-opens the window, + // it will take slightly longer because angular need to re-bootstrap + // the application. + // + // IMPORTANT: the angular application will automatically launch prompt + // windows via the tauri window interface. If we would call close(), + // those prompts wouldn't work anymore because the angular app would not + // be running in the background. + // + // - hide(): just set the window visibility to false. The advantage is that angular + // is still running and interacting with portmaster but it also means that + // we waste some system resources due to tauri window objects and the angular + // application. + + getCurrent().hide() + + return Promise.resolve(); + } + + // Tauri specific functions that are not defined in the IntegrationService interface. + // to use those methods you must check if integration instanceof TauriIntegrationService. + + async shouldShow(): Promise { + try { + const response = await invoke("plugin:portmaster|should_show"); + return response === "show"; + } catch (err) { + console.error(err); + return true; + } + } + + async shouldHandlePrompts(): Promise { + try { + const response = await invoke("plugin:portmaster|should_handle_prompts") + return response === "true" + } catch (err) { + console.error(err); + return false; + } + } + + get_state(key: string): Promise { + return invoke("plugin:portmaster|get_state"); + } + + set_state(key: string, value: string): Promise { + return invoke("plugin:portmaster|set_state", { + key, + value + }) + } + + getServiceManagerStatus(): Promise { + return asyncInvoke("plugin:portmaster|get_service_manager_status", {}) + } + + startService(): Promise { + return asyncInvoke("plugin:portmaster|start_service", {}); + } + + onExitRequest(cb: () => void): () => void { + let unlisten: () => void = () => { }; + + listen('exit-requested', () => { + cb(); + }).then(cleanup => { + unlisten = cleanup; + }) + + return () => { + unlisten(); + } + } + + openApp() { + Window.getByLabel("splash")?.close(); + const current = Window.getCurrent() + + current.isVisible() + .then(visible => { + if (!visible) { + current.show(); + current.setFocus(); + } + }); + } + + closePrompt() { + Window.getByLabel("prompt")?.close(); + } + + openPrompt() { + if (!this.withPrompts) { + return; + } + + if (Window.getByLabel("prompt")) { + return; + } + + let promptWindow = new Window("prompt", { + alwaysOnTop: true, + decorations: false, + minimizable: false, + maximizable: false, + resizable: false, + title: 'Portmaster Prompt', + visible: false, // the prompt marks it self as visible. + skipTaskbar: true, + closable: false, + center: true, + width: 600, + height: 300, + + // in src/main.ts we check the current location path + // and if it matches /prompt, we bootstrap the PromptEntryPointComponent + // instead of the AppComponent. + url: `http://${window.location.host}/prompt`, + } as any) + + promptWindow.once("tauri://error", (err) => { + console.error(err); + }); + } +} diff --git a/desktop/angular/src/app/intro/index.ts b/desktop/angular/src/app/intro/index.ts new file mode 100644 index 00000000..b0328f4b --- /dev/null +++ b/desktop/angular/src/app/intro/index.ts @@ -0,0 +1 @@ +export * from './intro.module'; diff --git a/desktop/angular/src/app/intro/intro.module.ts b/desktop/angular/src/app/intro/intro.module.ts new file mode 100644 index 00000000..8be0f36b --- /dev/null +++ b/desktop/angular/src/app/intro/intro.module.ts @@ -0,0 +1,36 @@ +import { OverlayModule } from "@angular/cdk/overlay"; +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { SfngDropDownModule, SfngTipUpModule, StepperConfig } from "@safing/ui"; +import { ConfigModule } from "../shared/config"; +import { Step1WelcomeComponent } from "./step-1-welcome"; +import { Step2TrackersComponent } from "./step-2-trackers"; +import { Step3DNSComponent } from "./step-3-dns"; +import { Step4TipupsComponent } from "./step-4-tipups"; + +const steps = [ + Step1WelcomeComponent, + Step2TrackersComponent, + Step3DNSComponent, + Step4TipupsComponent, +] + +@NgModule({ + imports: [ + CommonModule, + OverlayModule, + FormsModule, + SfngDropDownModule, + ConfigModule, + SfngTipUpModule, + ], + declarations: steps +}) +export class IntroModule { + static Stepper: StepperConfig = { + steps: steps, + canAbort: (idx) => idx === 0, + } +} + diff --git a/desktop/angular/src/app/intro/step-1-welcome/index.ts b/desktop/angular/src/app/intro/step-1-welcome/index.ts new file mode 100644 index 00000000..4261731e --- /dev/null +++ b/desktop/angular/src/app/intro/step-1-welcome/index.ts @@ -0,0 +1 @@ +export * from './step-1-welcome'; diff --git a/desktop/angular/src/app/intro/step-1-welcome/step-1-welcome.html b/desktop/angular/src/app/intro/step-1-welcome/step-1-welcome.html new file mode 100644 index 00000000..2c8010c6 --- /dev/null +++ b/desktop/angular/src/app/intro/step-1-welcome/step-1-welcome.html @@ -0,0 +1,14 @@ +

Portmaster Protects Your Privacy

+ +

+ Portmaster enhances your privacy with powerful defaults - no configuration needed! Of course you can customize + everything to your specific needs. +

+ + + + + diff --git a/desktop/angular/src/app/intro/step-1-welcome/step-1-welcome.ts b/desktop/angular/src/app/intro/step-1-welcome/step-1-welcome.ts new file mode 100644 index 00000000..e6af4a15 --- /dev/null +++ b/desktop/angular/src/app/intro/step-1-welcome/step-1-welcome.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component, Inject, TemplateRef, ViewChild } from "@angular/core"; +import { Step, StepRef, STEP_REF } from "@safing/ui"; +import { of } from "rxjs"; + +@Component({ + templateUrl: './step-1-welcome.html', + styleUrls: ['../step.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class Step1WelcomeComponent implements Step { + validChange = of(true) + + readonly nextButtonLabel = 'Quick Setup'; + + @ViewChild('buttonTemplate', { static: true }) + buttonTemplate!: TemplateRef; + + constructor( + @Inject(STEP_REF) public stepRef: StepRef, + ) { } +} + diff --git a/desktop/angular/src/app/intro/step-2-trackers/index.ts b/desktop/angular/src/app/intro/step-2-trackers/index.ts new file mode 100644 index 00000000..60b7451b --- /dev/null +++ b/desktop/angular/src/app/intro/step-2-trackers/index.ts @@ -0,0 +1 @@ +export * from './step-2-trackers' diff --git a/desktop/angular/src/app/intro/step-2-trackers/step-2-trackers.html b/desktop/angular/src/app/intro/step-2-trackers/step-2-trackers.html new file mode 100644 index 00000000..bfd16cd1 --- /dev/null +++ b/desktop/angular/src/app/intro/step-2-trackers/step-2-trackers.html @@ -0,0 +1,11 @@ +

Trackers Are Blocked System-Wide

+ +

Portmaster automatically blocks ads, trackers and malware hosts on your whole device. Portmaster knows what to block + through trusted domain lists, which are also used by Ad-Blockers in browsers, etc. You can always customize this in + the settings.

+ + + + + diff --git a/desktop/angular/src/app/intro/step-2-trackers/step-2-trackers.ts b/desktop/angular/src/app/intro/step-2-trackers/step-2-trackers.ts new file mode 100644 index 00000000..82d5be4d --- /dev/null +++ b/desktop/angular/src/app/intro/step-2-trackers/step-2-trackers.ts @@ -0,0 +1,48 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, ElementRef, OnInit, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ConfigService, Setting } from "@safing/portmaster-api"; +import { Step } from "@safing/ui"; +import { of } from "rxjs"; +import { mergeMap } from "rxjs/operators"; +import { SaveSettingEvent } from "src/app/shared/config/generic-setting"; + +@Component({ + templateUrl: './step-2-trackers.html', + styleUrls: ['../step.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class Step2TrackersComponent implements Step, OnInit { + private destroyRef = inject(DestroyRef); + + validChange = of(true) + + setting: Setting | null = null; + + constructor( + public configService: ConfigService, + public readonly elementRef: ElementRef, + private cdr: ChangeDetectorRef, + ) { } + + ngOnInit(): void { + this.configService.get('filter/lists') + .pipe( + mergeMap(setting => { + this.setting = setting; + + return this.configService.watch(setting.Key) + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(value => { + this.setting!.Value = value; + + this.cdr.markForCheck(); + }); + } + + saveSetting(event: SaveSettingEvent) { + this.configService.save(event.key, event.value) + .subscribe() + } +} diff --git a/desktop/angular/src/app/intro/step-3-dns/index.ts b/desktop/angular/src/app/intro/step-3-dns/index.ts new file mode 100644 index 00000000..85ccdb1a --- /dev/null +++ b/desktop/angular/src/app/intro/step-3-dns/index.ts @@ -0,0 +1 @@ +export * from './step-3-dns' diff --git a/desktop/angular/src/app/intro/step-3-dns/step-3-dns.html b/desktop/angular/src/app/intro/step-3-dns/step-3-dns.html new file mode 100644 index 00000000..aa17288a --- /dev/null +++ b/desktop/angular/src/app/intro/step-3-dns/step-3-dns.html @@ -0,0 +1,17 @@ +

Secure DNS for All Connections

+ +

Portmaster automatically encrypts all your DNS queries to safeguard them from prying eyes. Portmaster sets a default + provider, but you can always switch to a custom DNS-over-TLS provider in the global settings.

+ + +
+ + + +
+
diff --git a/desktop/angular/src/app/intro/step-3-dns/step-3-dns.ts b/desktop/angular/src/app/intro/step-3-dns/step-3-dns.ts new file mode 100644 index 00000000..a1dddae6 --- /dev/null +++ b/desktop/angular/src/app/intro/step-3-dns/step-3-dns.ts @@ -0,0 +1,106 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, ElementRef, OnInit, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ConfigService, QuickSetting, Setting, applyQuickSetting } from "@safing/portmaster-api"; +import { Step } from "@safing/ui"; +import { of } from "rxjs"; +import { mergeMap } from "rxjs/operators"; +import { SaveSettingEvent } from "src/app/shared/config/generic-setting"; + +interface QuickSettingModel extends QuickSetting { + active: boolean; +} + +@Component({ + templateUrl: './step-3-dns.html', + styleUrls: ['../step.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class Step3DNSComponent implements Step, OnInit { + private destroyRef = inject(DestroyRef); + + validChange = of(true) + + setting: Setting | null = null; + quickSettings: QuickSettingModel[] = []; + isCustomValue = false; + + constructor( + public configService: ConfigService, + public readonly elementRef: ElementRef, + private cdr: ChangeDetectorRef, + ) { } + + private getQuickSettings(): QuickSettingModel[] { + if (!this.setting) { + return []; + } + + let val = this.setting.Annotations["safing/portbase:ui:quick-setting"]; + if (val === undefined) { + return []; + } + + if (!Array.isArray(val)) { + return [{ + ...val, + active: false, + }] + } + + return val.map(v => ({ + ...v, + active: false, + })) + } + + ngOnInit(): void { + this.configService.get('dns/nameservers') + .pipe( + mergeMap(setting => { + this.setting = setting; + this.quickSettings = this.getQuickSettings(); + return this.configService.watch(setting.Key) + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(value => { + this.setting!.Value = value; + + let hasActive = false; + this.isCustomValue = false; + + this.quickSettings.forEach(setting => { + if (this.setting?.Value !== undefined && JSON.stringify(this.setting.Value) === JSON.stringify(setting.Value)) { + setting.active = true; + hasActive = true; + } else { + setting.active = false; + } + }); + + if (!hasActive) { + if (this.setting?.Value !== undefined && JSON.stringify(this.setting!.Value) !== JSON.stringify(this.setting!.DefaultValue)) { + this.isCustomValue = true; + } else if (this.quickSettings.length > 0) { + this.quickSettings[0].active = true; + } + } + + this.cdr.markForCheck(); + }); + } + + saveSetting(event: SaveSettingEvent) { + this.configService.save(event.key, event.value) + .subscribe() + } + + applyQuickSetting(action: QuickSetting) { + const newValue = applyQuickSetting( + this.setting!.Value || this.setting!.DefaultValue, + action, + ) + this.configService.save(this.setting!.Key, newValue) + .subscribe(); + } +} diff --git a/desktop/angular/src/app/intro/step-4-tipups/index.ts b/desktop/angular/src/app/intro/step-4-tipups/index.ts new file mode 100644 index 00000000..02886c50 --- /dev/null +++ b/desktop/angular/src/app/intro/step-4-tipups/index.ts @@ -0,0 +1 @@ +export * from './step-4-tipups' diff --git a/desktop/angular/src/app/intro/step-4-tipups/step-4-tipups.html b/desktop/angular/src/app/intro/step-4-tipups/step-4-tipups.html new file mode 100644 index 00000000..f15afd36 --- /dev/null +++ b/desktop/angular/src/app/intro/step-4-tipups/step-4-tipups.html @@ -0,0 +1,11 @@ +

Learn More as You Explore

+ +

Portmaster has a lot more to offer. When you decide to dive deeper you can always click on an information icon to + learn more about a certain feature. Look out for those!

+ +
+ Click Me! +
+ +
+
diff --git a/desktop/angular/src/app/intro/step-4-tipups/step-4-tipups.ts b/desktop/angular/src/app/intro/step-4-tipups/step-4-tipups.ts new file mode 100644 index 00000000..5b0463a1 --- /dev/null +++ b/desktop/angular/src/app/intro/step-4-tipups/step-4-tipups.ts @@ -0,0 +1,12 @@ +import { ChangeDetectionStrategy, Component } from "@angular/core"; +import { Step } from "@safing/ui"; +import { of } from "rxjs"; + +@Component({ + templateUrl: './step-4-tipups.html', + styleUrls: ['../step.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class Step4TipupsComponent implements Step { + validChange = of(true) +} diff --git a/desktop/angular/src/app/intro/step.scss b/desktop/angular/src/app/intro/step.scss new file mode 100644 index 00000000..1d17d9a2 --- /dev/null +++ b/desktop/angular/src/app/intro/step.scss @@ -0,0 +1,11 @@ +:host { + @apply flex flex-col items-center justify-center; +} + +h1 { + @apply text-primary text-2xl font-medium capitalize text-center py-5; +} + +p { + @apply text-tertiary text-sm font-medium text-center; +} diff --git a/desktop/angular/src/app/layout/navigation/navigation.html b/desktop/angular/src/app/layout/navigation/navigation.html new file mode 100644 index 00000000..0359b2dd --- /dev/null +++ b/desktop/angular/src/app/layout/navigation/navigation.html @@ -0,0 +1,230 @@ +
+
+ + + + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+ + + + +
+ + + + diff --git a/desktop/angular/src/app/layout/navigation/navigation.scss b/desktop/angular/src/app/layout/navigation/navigation.scss new file mode 100644 index 00000000..4683bec5 --- /dev/null +++ b/desktop/angular/src/app/layout/navigation/navigation.scss @@ -0,0 +1,98 @@ +:host { + height: 100vh; + top: 0px; + position: sticky; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + user-select: none; + + .logo-image { + @apply w-6 -top-3 -left-3 absolute; + position: absolute; + } + + svg { + &:not(.connected) { + animation-timing-function: cubic-bezier(0.445, 0.05, 0.55, 0.95); + + path.inner { + fill: theme('colors.info.red'); + } + } + } + + div.nav-list { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + } + + div.nav-lower-list { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + padding-bottom: 1.5rem; + } + + div.link { + @apply my-2; + + width: 2rem; + height: 2rem; + border-radius: 10px; + + display: flex; + justify-content: space-around; + align-items: center; + + cursor: pointer; + + & { + outline: none; + + svg, + fa-icon { + opacity: .5; + } + } + + &:target, + &.active { + background-color: #2c2c2c; + + svg, + fa-icon { + opacity: 1; + transform: scale(1.08); + } + } + + &:hover { + + svg, + fa-icon { + opacity: 1; + } + } + + svg, + fa-icon { + + &.dash, + &.spn, + &.monitor, + &.app, + &.help, + &.settings { + @apply text-white; + width: 1.1rem; + position: relative; + stroke: currentColor; + } + } + } +} diff --git a/desktop/angular/src/app/layout/navigation/navigation.ts b/desktop/angular/src/app/layout/navigation/navigation.ts new file mode 100644 index 00000000..4752301f --- /dev/null +++ b/desktop/angular/src/app/layout/navigation/navigation.ts @@ -0,0 +1,298 @@ +import { INTEGRATION_SERVICE, IntegrationService } from 'src/app/integration'; +import { ConnectedPosition } from '@angular/cdk/overlay'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, OnInit, Output, inject } from '@angular/core'; +import { ConfigService, DebugAPI, PortapiService, SPNService, StringSetting } from '@safing/portmaster-api'; +import { tap } from 'rxjs/operators'; +import { AppComponent } from 'src/app/app.component'; +import { NotificationType, NotificationsService, StatusService, VersionStatus } from 'src/app/services'; +import { ActionIndicatorService } from 'src/app/shared/action-indicator'; +import { fadeInAnimation, fadeOutAnimation } from 'src/app/shared/animations'; +import { ExitService } from 'src/app/shared/exit-screen'; +import { TauriIntegrationService } from 'src/app/integration/taur-app'; + +@Component({ + selector: 'app-navigation', + templateUrl: './navigation.html', + styleUrls: ['./navigation.scss'], + exportAs: 'navigation', + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + fadeInAnimation, + fadeOutAnimation, + ] +}) +export class NavigationComponent implements OnInit { + private readonly integration = inject(INTEGRATION_SERVICE); + + /** Emits the current portapi connection state on changes. */ + readonly connected$ = this.portapi.connected$; + + /** @private The available and selected resource versions. */ + versions: VersionStatus | null = null; + + /** Whether or not we have new, unseen notifications */ + hasNewNotifications = false; + + /** The color to use for the notifcation-available hint (dot) */ + notificationColor: string = 'text-green-300'; + + /** Whether or not we have new, unseen prompts */ + hasNewPrompts = false; + + /** Whether or not prompting is globally enabled. */ + globalPromptingEnabled = false; + + @Output() + sideDashChange = new EventEmitter<'collapsed' | 'expanded' | 'force-overlay'>(); + + /** Whether or not the side dash should be expanded or collapsed */ + sideDashStatus: 'collapsed' | 'expanded' = 'expanded'; + + constructor( + private portapi: PortapiService, + private exitService: ExitService, + private statusService: StatusService, + private configService: ConfigService, + private appComponent: AppComponent, + private debugAPI: DebugAPI, + private actionIndicator: ActionIndicatorService, + private notificationService: NotificationsService, + private spnService: SPNService, + private cdr: ChangeDetectorRef + ) { } + + dropDownPositions: ConnectedPosition[] = [ + { + originX: 'end', + originY: 'top', + overlayX: 'start', + overlayY: 'top' + } + ] + + ngOnInit() { + const mql = window.matchMedia('(max-width: 1200px)'); + + if (mql.matches) { + this.sideDashStatus = 'collapsed'; + this.sideDashChange.next(this.sideDashStatus); + } + + mql.addEventListener('change', () => { + if (mql.matches) { + this.sideDashStatus = 'collapsed'; + } else { + this.sideDashStatus = 'expanded'; + } + this.sideDashChange.next(this.sideDashStatus); + }) + + this.statusService.getVersions() + .subscribe(versions => { + this.versions = versions; + this.cdr.markForCheck(); + }); + + this.configService.watch('filter/defaultAction') + .subscribe(defaultAction => { + this.globalPromptingEnabled = defaultAction === 'ask'; + this.cdr.markForCheck(); + }) + + this.notificationService.new$ + .subscribe(notif => { + + + if (notif.some(n => n.Type === NotificationType.Prompt && n.EventID.startsWith("filter:prompt"))) { + this.hasNewPrompts = true; + + if (this.integration instanceof TauriIntegrationService) { + this.integration.openPrompt(); + } + } else { + this.hasNewPrompts = false; + + if (this.integration instanceof TauriIntegrationService) { + this.integration.closePrompt(); + } + } + + if (notif.some(n => !n.EventID.startsWith("filter:prompt"))) { + this.hasNewNotifications = true; + } else { + this.hasNewNotifications = false; + } + + if (notif.some(n => n.Type === NotificationType.Error)) { + this.notificationColor = 'text-red-300'; + } else if (notif.some(n => n.Type === NotificationType.Warning)) { + this.notificationColor = 'text-yellow-300'; + } else { + this.notificationColor = 'text-green-300'; + } + + this.cdr.markForCheck(); + }) + } + + toggleSideDash(event: MouseEvent) { + let notify: 'expanded' | 'collapsed' | 'force-overlay' = this.sideDashStatus; + + if (this.sideDashStatus === 'collapsed') { + this.sideDashStatus = 'expanded'; + notify = 'expanded'; + if (event.shiftKey) { + notify = 'force-overlay' + } + } else { + this.sideDashStatus = 'collapsed'; + notify = 'collapsed' + } + + this.sideDashChange.next(notify); + } + + /** + * @private + * Injects a ui/reload event and performs a complete + * reload of the window once the portmaster re-opened the + * UI bundle. + */ + reloadUI(_: Event) { + this.portapi.reloadUI() + .pipe( + tap(() => { + setTimeout(() => window.location.reload(), 1000) + }) + ) + .subscribe(this.actionIndicator.httpObserver( + 'Reloading UI ...', + 'Failed to Reload UI', + )) + } + + /** Re-initialize the SPN */ + reinitSPN(_: Event) { + this.portapi.reinitSPN() + .subscribe(this.actionIndicator.httpObserver( + 'Re-initialized SPN', + 'Failed to re-initialize the SPN' + )) + } + + /** Logs the user out of the SPN completely by purgin the user profile from the local storage */ + logoutCompletely(_: Event) { + this.spnService.logout(true) + .subscribe(this.actionIndicator.httpObserver( + 'Logout', + 'You have been logged out of the SPN completely.' + )) + } + + /** + * @private + * Clear the DNS name cache. + */ + clearDNSCache(_: Event) { + this.portapi.clearDNSCache() + .subscribe(this.actionIndicator.httpObserver( + 'DNS Cache Cleared', + 'Failed to Clear DNS Cache.', + )) + } + + cleanupHistory(_: Event) { + this.portapi.cleanupHistory() + .subscribe(this.actionIndicator.httpObserver( + 'Network History Cleaned Up', + 'Failed to Cleanup Network History.' + )) + } + + /** + * @private + * Trigger downloading of updates + * + * @param event - The mouse event + */ + downloadUpdates(event: Event) { + this.portapi.checkForUpdates() + .subscribe(this.actionIndicator.httpObserver( + 'Downloading Updates ...', + 'Failed to Check for Updates', + )) + } + + /** + * @private + * Trigger a shutdown of the portmaster-core service + */ + shutdown(_: Event) { + this.exitService.shutdownPortmaster(); + } + + /** + * @private + * Trigger a restart of the portmaster-core service. Requires + * that portmaster has been started with a service-wrapper. + * + * @param event The mouse event + */ + restart(event: Event) { + // prevent default and stop-propagation to avoid + // expanding the accordion body. + event.preventDefault(); + event.stopPropagation(); + + this.portapi.restartPortmaster() + .subscribe(this.actionIndicator.httpObserver( + 'Restarting ...', + 'Failed to Restart', + )) + } + + /** + * @private + * Opens the data-directory of the portmaster installation. + * Requires the application to run inside electron. + */ + async openDataDir(event: Event) { + const dir = await this.integration.getInstallDir() + await this.integration.openExternal(dir); + } + + openChangeLog() { + const url = "https://github.com/safing/portmaster/releases"; + this.integration.openExternal(url); + } + + showIntro() { + this.appComponent.showIntro() + } + + resetBroadcastState() { + this.portapi.resetBroadcastState() + .subscribe(this.actionIndicator.httpObserver( + 'Broadcast State Cleared', + 'Failed to Reset Broadcast State.', + )) + } + + copyDebugInfo(event: Event) { + // prevent default and stop-propagation to avoid + // expanding the accordion body. + event.preventDefault(); + event.stopPropagation(); + + this.debugAPI.getCoreDebugInfo() + .subscribe( + async info => { + await this.integration.writeToClipboard(info); + }, + err => { + console.error(err); + this.actionIndicator.error('Failed loading debug data', err); + } + ) + } +} diff --git a/desktop/angular/src/app/layout/side-dash/side-dash.html b/desktop/angular/src/app/layout/side-dash/side-dash.html new file mode 100644 index 00000000..81e8b15f --- /dev/null +++ b/desktop/angular/src/app/layout/side-dash/side-dash.html @@ -0,0 +1,10 @@ +
+ +
+ + + + + + + diff --git a/desktop/angular/src/app/layout/side-dash/side-dash.scss b/desktop/angular/src/app/layout/side-dash/side-dash.scss new file mode 100644 index 00000000..6862c275 --- /dev/null +++ b/desktop/angular/src/app/layout/side-dash/side-dash.scss @@ -0,0 +1,11 @@ +:host { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + overflow: hidden; + overflow-y: hidden; + width: 419px; + + @apply pt-4; +} diff --git a/desktop/angular/src/app/layout/side-dash/side-dash.ts b/desktop/angular/src/app/layout/side-dash/side-dash.ts new file mode 100644 index 00000000..c4836634 --- /dev/null +++ b/desktop/angular/src/app/layout/side-dash/side-dash.ts @@ -0,0 +1,13 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'app-side-dash', + templateUrl: './side-dash.html', + styleUrls: ['./side-dash.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SideDashComponent { + /** Whether or not a SPN account login is required */ + spnLoginRequired = false; + +} diff --git a/desktop/angular/src/app/package-lock.json b/desktop/angular/src/app/package-lock.json new file mode 100644 index 00000000..4ef966ec --- /dev/null +++ b/desktop/angular/src/app/package-lock.json @@ -0,0 +1,27 @@ +{ + "name": "portmaster", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "portmaster", + "devDependencies": { + "@types/node": "^17.0.31" + } + }, + "node_modules/@types/node": { + "version": "17.0.31", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.31.tgz", + "integrity": "sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q==", + "dev": true + } + }, + "dependencies": { + "@types/node": { + "version": "17.0.31", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.31.tgz", + "integrity": "sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q==", + "dev": true + } + } +} diff --git a/desktop/angular/src/app/package.json b/desktop/angular/src/app/package.json new file mode 100644 index 00000000..119f6216 --- /dev/null +++ b/desktop/angular/src/app/package.json @@ -0,0 +1,12 @@ +{ + "name": "portmaster", + "private": true, + "description_1": "This is a special package.json file that is not used by package managers.", + "description_2": "It is used to tell the tools and bundlers whether the code under this directory is free of code with non-local side-effect. Any code that does have non-local side-effects can't be well optimized (tree-shaken) and will result in unnecessary increased payload size.", + "description_3": "It should be safe to set this option to 'false' for new applications, but existing code bases could be broken when built with the production config if the application code does contain non-local side-effects that the application depends on.", + "description_4": "To learn more about this file see: https://angular.io/config/app-package-json.", + "sideEffects": false, + "devDependencies": { + "@types/node": "^17.0.31" + } +} diff --git a/desktop/angular/src/app/pages/app-view/app-insights/app-insights.component.html b/desktop/angular/src/app/pages/app-view/app-insights/app-insights.component.html new file mode 100644 index 00000000..32ab491c --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/app-insights/app-insights.component.html @@ -0,0 +1,13 @@ +
+ + + + + + + + + + + +
diff --git a/desktop/angular/src/app/pages/app-view/app-insights/app-insights.component.ts b/desktop/angular/src/app/pages/app-view/app-insights/app-insights.component.ts new file mode 100644 index 00000000..264a5615 --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/app-insights/app-insights.component.ts @@ -0,0 +1,96 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, Input, OnInit, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AppProfile, BandwidthChartResult, ChartResult, Netquery } from '@safing/portmaster-api'; +import { repeat } from 'rxjs'; +import { CircularBarChartConfig, splitQueryResult } from 'src/app/shared/netquery/circular-bar-chart/circular-bar-chart.component'; +import { DefaultBandwidthChartConfig } from 'src/app/shared/netquery/line-chart/line-chart'; + +interface CountryBarData { + series: 'country'; + value: number; + country: string; +} + +@Component({ + selector: 'app-app-insights', + templateUrl: './app-insights.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AppInsightsComponent implements OnInit { + private readonly netquery = inject(Netquery); + private readonly destroyRef = inject(DestroyRef); + private readonly cdr = inject(ChangeDetectorRef); + + @Input() + profile!: AppProfile; + + connectionChart: ChartResult[] = []; + + bandwidthChart: BandwidthChartResult[] = []; + + bwChartConfig = DefaultBandwidthChartConfig; + + countryData: CountryBarData[] = []; + + readonly countryBarConfig: CircularBarChartConfig = { + stack: 'country', + seriesKey: 'series', + value: 'value', + ticks: 3, + colorAsClass: true, + series: { + 'count': { + color: 'text-green-300 text-opacity-50', + }, + }, + } + + ngOnInit() { + const key = `${this.profile.Source}/${this.profile.ID}` + + this.netquery.batch({ + countryData: { + select: [ + 'country', + { $count: { field: '*', as: 'count' } }, + ], + query: { + internal: { $eq: false }, + country: { $ne: '' } + }, + groupBy: ['country'] + } + }) + .pipe( + repeat({ delay: 10000 }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(result => { + this.countryData = splitQueryResult(result.countryData, ['count']) as CountryBarData[]; + console.log(this.countryData) + this.cdr.markForCheck(); + }) + + this.netquery.activeConnectionChart({ profile: key }) + .pipe( + repeat({ delay: 10000 }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(data => { + this.connectionChart = data; + this.cdr.markForCheck(); + }) + + this.netquery.bandwidthChart({ profile: key }, undefined, 60) + .pipe( + repeat({ delay: 10000 }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(data => { + this.bandwidthChart = data; + this.cdr.markForCheck(); + }) + + } + +} diff --git a/desktop/angular/src/app/pages/app-view/app-view.html b/desktop/angular/src/app/pages/app-view/app-view.html new file mode 100644 index 00000000..8003881c --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/app-view.html @@ -0,0 +1,425 @@ + + +
+ +
+ Apps + + + + + + {{ appProfile.Name }} +
+ + + +
+ + + +
+
+ +
+ +

+ + + {{appProfile!.Name}} +

+ + +
+
+ Path: + + {{ applicationDirectory }} + +
+
+ Binary: + + {{ binaryName }} + +
+
+ Active Connections: + {{stats?.countAliveConnections || 0}} +
+
+ Network History: + + As of {{ historyAvailableSince | date }} + Remove all {{ connectionsInHistory }} Connections + + + None + +
+
+ + +
+ + + + + + + + + + + + + + + + Edit App Profile + Export App Profile + Delete App Profile + + + +
+
+ + +
+
+

{{ stats!.size | prettyCount }}

+ Connections +
+ +
+

{{ (100 / stats!.size) * (stats!.size + - stats!.countAllowed) | number:'1.0-1' }}%

+ Blocked +
+ +
+

+ {{ stats.bytes_received | bytes }} +

+ + + Available in Plus + + + Received +
+ +
+

+ {{ stats.bytes_sent | bytes }} +

+ Sent +
+ +
+
+ +
+ + + +
+
+ + + + +
+ + +
+
+ + +
+ +
+ + + + + + + Get Help + + +
+ + + + View All + + + View Active + + +
+
+ +
+
+ App Specific Settings + +
+
+ + + + + + + +
+ + + + +

+ + {{ appProfile!.Name }} + + is fully using the global settings. +

+

+ Start creating exceptions for it now. +

+ +
+
+
+
+ + + +
+ +
+
+

+ + {{appProfile!.Name}} +

+

+ + {{appProfile!.PresentationPath}} +

+
+ +
+

+ + {{appProfile!.Created * 1000 | date:'medium'}} +

+

+ + {{appProfile!.LastEdited * 1000 | date:'medium'}} + N/A +

+
+ + +
+

+ + {{!!appProfile!.Internal ? 'yes' : 'no'}} +

+

+ + {{appProfile!.Source}} +

+

+ + {{appProfile!.ID}} +

+
+ +
+

+ + {{layeredProfile?.RevisionCounter}} +

+

+ + +

    +
  1. + {{layer}} +
  2. +
+ +

+
+
+
+ + +
+

+ + + + Description + +

+ + + +
+ + +
+

+ + + + Warning + +

+ + + + updated + {{ appProfile.WarningLastUpdated | timeAgo }} +
+ + +
+

+ + + + + Fingerprints + +

+ + This profile will be applied to processes that match one of the following + fingerprints: + +
+ + + + + + + + {{ fp.Type }} + + + where + {{ fp.Type === 'tag' ? (tagNames[fp.Key] || fp.Key) : fp.Key }} + + + {{ fp.Operation }} + {{ fp.Value }} +
+
+ + +
+

+ + + + Delete Profile + +

+ + You can completely delete this profile to get rid of any settings. The profile + will + be automatically re-created with default settings as soon as the application starts to use the + network. + + +
+ + +
+

+ + + + + + + Debugging + +

+ + When reporting issues with this app please make sure to include the + following + debug information: + + +
+
+
+ +
+ +
+
+
+
+ + diff --git a/desktop/angular/src/app/pages/app-view/app-view.scss b/desktop/angular/src/app/pages/app-view/app-view.scss new file mode 100644 index 00000000..977c3b72 --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/app-view.scss @@ -0,0 +1,3 @@ +:host { + @apply flex flex-col h-screen max-h-screen; +} diff --git a/desktop/angular/src/app/pages/app-view/app-view.ts b/desktop/angular/src/app/pages/app-view/app-view.ts new file mode 100644 index 00000000..85a365a2 --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/app-view.ts @@ -0,0 +1,641 @@ +import { + ChangeDetectorRef, + Component, + DestroyRef, + OnDestroy, + OnInit, + ViewChild, + inject, +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ActivatedRoute, Router } from '@angular/router'; +import { + AppProfile, + AppProfileService, + Condition, + ConfigService, + Database, + DebugAPI, + ExpertiseLevel, + FeatureID, + FlatConfigObject, + IProfileStats, + LayeredProfile, + Netquery, + PortapiService, + SPNService, + Setting, + flattenProfileConfig, + setAppSetting +} from '@safing/portmaster-api'; +import { SfngDialogService } from '@safing/ui'; +import { + BehaviorSubject, + Observable, + Subscription, + combineLatest, + interval, + of, + throwError, +} from 'rxjs'; +import { + catchError, + distinctUntilChanged, + map, + mergeMap, + startWith, + switchMap, +} from 'rxjs/operators'; +import { INTEGRATION_SERVICE } from 'src/app/integration'; +import { SessionDataService } from 'src/app/services'; +import { ActionIndicatorService } from 'src/app/shared/action-indicator'; +import { fadeInAnimation, fadeOutAnimation } from 'src/app/shared/animations'; +import { + ExportConfig, + ExportDialogComponent, +} from 'src/app/shared/config/export-dialog/export-dialog.component'; +import { SaveSettingEvent } from 'src/app/shared/config/generic-setting/generic-setting'; +import { ExpertiseService } from 'src/app/shared/expertise'; +import { SfngNetqueryViewer } from 'src/app/shared/netquery'; +import { EditProfileDialog } from './../../shared/edit-profile-dialog/edit-profile-dialog'; + +@Component({ + templateUrl: './app-view.html', + styleUrls: ['../page.scss', './app-view.scss'], + animations: [fadeOutAnimation, fadeInAnimation], +}) +export class AppViewComponent implements OnInit, OnDestroy { + private readonly integration = inject(INTEGRATION_SERVICE); + + @ViewChild(SfngNetqueryViewer) + netqueryViewer?: SfngNetqueryViewer; + + destroyRef = inject(DestroyRef); + spn = inject(SPNService); + + canUseHistory = false; + canViewBW = false; + canUseSPN = false; + + /** subscription to our update-process observable */ + private subscription = Subscription.EMPTY; + + /** + * @private + * historyAvailableSince holds the date of the oldes connection + * in the history database for this app. + */ + historyAvailableSince: Date | null = null; + + /** + * @private + * connectionsInHistory holds the total amount of connections + * in the history database for this app + */ + connectionsInHistory = 0; + + /** + * @private + * The current AppProfile we are showing. + */ + appProfile: AppProfile | null = null; + + /** + * @private + * Whether or not the overview componet should be rendered. + */ + get showOverview() { + return this.appProfile == null && !this._loading; + } + + /** + * @private + * The currently displayed list of settings + */ + settings: Setting[] = []; + + profileSettings: Setting[] = []; + + /** + * @private + * All available settings. + */ + allSettings: Setting[] = []; + + /** + * @private + * The current search term displayed in the search-input. + */ + searchTerm = ''; + + /** + * @private + * The key of the setting to highligh, if any ... + */ + highlightSettingKey: string | null = null; + + /** + * @private + * Emits whenever the currently used settings "view" changes. + */ + viewSettingChange = new BehaviorSubject<'all' | 'active'>('all'); + + /** + * @private + * The path of the application binary + */ + applicationDirectory = ''; + + /** + * @private + * The name of the binary + */ + binaryName = ''; + + /** + * @private + * Whether or not the profile warning message should be displayed + */ + displayWarning = false; + + /** + * @private + * The current profile statistics + */ + stats: IProfileStats | null = null; + + /** + * @private + * The internal, layered profile if the app is active + */ + layeredProfile: LayeredProfile | null = null; + + /** Used to track whether we are already initialized */ + private _loading = true; + + /** + * @private + * + * Defines what "view" we are currently in + */ + get viewSetting(): 'all' | 'active' { + return this.viewSettingChange.getValue(); + } + + /** A lookup map from tag ID to tag Name */ + tagNames: { + [tagID: string]: string; + } = {}; + + collapseHeader = false; + + constructor( + public sessionDataService: SessionDataService, + private profileService: AppProfileService, + private route: ActivatedRoute, + private netquery: Netquery, + private cdr: ChangeDetectorRef, + private configService: ConfigService, + private router: Router, + private actionIndicator: ActionIndicatorService, + private dialog: SfngDialogService, + private debugAPI: DebugAPI, + private expertiseService: ExpertiseService, + private portapi: PortapiService + ) { } + + /** + * @private + * Used to save a change in the app settings. Emitted by the config-view + * component + * + * @param event The emitted save-settings-event. + */ + saveSetting(event: SaveSettingEvent) { + // Guard against invalid usage and abort if there's not appProfile + // to save. + if (!this.appProfile) { + return; + } + + if (!this.appProfile!.Config) { + this.appProfile.Config = {} + } + + // If the value has been "reset to global value" we need to + // set the value to "undefined". + if (event.isDefault) { + setAppSetting(this.appProfile!.Config, event.key, undefined); + } else { + setAppSetting(this.appProfile!.Config, event.key, event.value); + } + + // Actually safe the profile + this.profileService.saveProfile(this.appProfile!).subscribe({ + next: () => { + if (!!event.accepted) { + event.accepted(); + } + }, + error: (err) => { + // if there's a callback function for errors call it. + if (!!event.rejected) { + event.rejected(err); + } + + console.error(err); + this.actionIndicator.error('Failed to save setting', err); + }, + }); + } + + exportProfile() { + if (!this.appProfile) { + return; + } + + this.portapi + .exportProfile(`${this.appProfile.Source}/${this.appProfile.ID}`) + .subscribe((exportBlob) => { + const exportConfig: ExportConfig = { + type: 'profile', + content: exportBlob, + }; + + this.dialog.create(ExportDialogComponent, { + data: exportConfig, + autoclose: false, + backdrop: true, + }); + }); + } + + editProfile() { + if (!this.appProfile) { + return; + } + + this.dialog + .create(EditProfileDialog, { + backdrop: true, + autoclose: false, + data: `${this.appProfile.Source}/${this.appProfile.ID}`, + }) + .onAction('deleted', () => { + // navigate to the app overview if it has been deleted. + this.router.navigate(['/app/']); + }); + } + + cleanProfileHistory() { + if (!this.appProfile) { + return; + } + + const observer = this.actionIndicator.httpObserver( + 'History successfully removed', + 'Failed to remove history' + ); + + this.netquery + .cleanProfileHistory(this.appProfile.Source + '/' + this.appProfile.ID) + .subscribe({ + next: (res) => { + observer.next!(res); + this.historyAvailableSince = null; + this.connectionsInHistory = 0; + this.cdr.markForCheck(); + }, + error: (err) => { + observer.error!(err); + }, + }); + } + + ngOnInit() { + this.profileService.tagDescriptions().subscribe((tags) => { + tags.forEach((t) => { + this.tagNames[t.ID] = t.Name; + this.cdr.markForCheck(); + }); + }); + + // watch the route parameters and start watching the referenced + // application profile, it's layer profile and polling the stats. + const profileStream: Observable< + [AppProfile, LayeredProfile | null, IProfileStats | null] | null + > = this.route.paramMap.pipe( + switchMap((params) => { + // Get the profile source and id. If one is unset (null) + // than return a"null" emit-once stream. + const source = params.get('source'); + const id = params.get('id'); + if (source === null || id === null) { + this._loading = false; + return of(null); + } + this._loading = true; + + this.historyAvailableSince = null; + this.connectionsInHistory = 0; + this.appProfile = null; + this.stats = null; + + // Start watching the application profile. + // switchMap will unsubscribe automatically if + // we start watching a different profile. + return this.profileService.getAppProfile(source, id).pipe( + catchError((err) => { + if (typeof err === 'string') { + err = new Error(err); + } + + this.router.navigate(['/app/overview'], { + onSameUrlNavigation: 'reload', + }); + + this.actionIndicator.error( + 'Failed To Get Profile', + this.actionIndicator.getErrorMessgae(err) + ); + + return throwError(() => err); + }), + mergeMap(() => { + return combineLatest([ + this.profileService.watchAppProfile(source, id), + this.profileService + .watchLayeredProfile(source, id) + .pipe(startWith(null)), + interval(10000).pipe( + startWith(-1), + mergeMap(() => + this.netquery + .getProfileStats({ + profile: `${source}/${id}`, + }) + .pipe(map((result) => result?.[0])) + ), + startWith(null) + ), + ]); + }) + ); + }) + ); + + // used to track changes to the object identity of the global configuration + let prevousGlobal: FlatConfigObject = {}; + + this.subscription = combineLatest([ + profileStream, // emits the current app profile everytime it changes + this.route.queryParamMap, // for changes to the settings= query parameter + this.profileService.globalConfig(), // for changes to ghe global profile + this.configService.query(''), // get ALL settings (once, only the defintion is of intereset) + this.viewSettingChange.pipe( + // watch the current "settings-view" setting, but only if it changes + distinctUntilChanged() + ), + ]).subscribe( + async ([profile, queryMap, global, allSettings, viewSetting]) => { + const previousProfile = this.appProfile; + + if (!!profile) { + const key = profile![0].Source + '/' + profile![0].ID; + + const query: Condition = { + profile: key, + }; + + // ignore internal connections if the user is not in developer mode. + if (this.expertiseService.currentLevel !== ExpertiseLevel.Developer) { + query.internal = { + $eq: false, + }; + } + + this.netquery + .query( + { + select: [ + { + $min: { + field: 'started', + as: 'first_connection', + }, + }, + { + $count: { + field: '*', + as: 'totalCount', + }, + }, + ], + groupBy: ['profile'], + query: { + profile: `${profile[0].Source}/${profile[0].ID}`, + }, + databases: [Database.History], + }, + 'app-view-get-first-connection' + ) + .subscribe((result) => { + if (result.length > 0) { + this.historyAvailableSince = new Date( + result[0].first_connection! + ); + this.connectionsInHistory = result[0].totalCount; + } else { + this.historyAvailableSince = null; + this.connectionsInHistory = 0; + } + + this.cdr.markForCheck(); + }); + + this.appProfile = profile[0] || null; + this.layeredProfile = profile[1] || null; + this.stats = profile[2] || null; + } else { + this.appProfile = null; + this.layeredProfile = null; + this.stats = null; + } + + this.displayWarning = false; + + if (this.appProfile?.WarningLastUpdated) { + const now = new Date().getTime(); + const diff = + now - new Date(this.appProfile.WarningLastUpdated).getTime(); + this.displayWarning = diff < 1000 * 60 * 60 * 24 * 7; + } + + if (!!this.netqueryViewer && this._loading) { + this.netqueryViewer.performSearch(); + } + + this._loading = false; + + if (!!this.appProfile?.PresentationPath) { + let parts: string[] = []; + let sep = '/'; + if (this.appProfile.PresentationPath[0] === '/') { + // linux, darwin, bsd ... + sep = '/'; + } else { + // windows ... + sep = '\\'; + } + parts = this.appProfile.PresentationPath.split(sep); + + this.binaryName = parts.pop()!; + this.applicationDirectory = parts.join(sep); + } else { + this.applicationDirectory = ''; + this.binaryName = ''; + } + + this.highlightSettingKey = queryMap.get('setting'); + let profileConfig: FlatConfigObject = {}; + + // if we have a profile flatten it's configuration map to something + // more useful. + if (!!this.appProfile) { + profileConfig = flattenProfileConfig(this.appProfile.Config); + } + + // if we should highlight a setting make sure to switch the + // viewSetting to all if it's the "global" default (that is, no + // value is set). Otherwise the setting won't render and we cannot + // highlight it. + // We need to keep this even though we default to "all" now since + // the following might happen: + // - user already navigated to an app-page and selected "View Active". + // - a notification comes in that has a "show setting" action + // - the user clicks the action button and the setting should be displayed + // - since the requested setting has not been changed it is not available + // in "View Active" so we need to switch back to "View All". Otherwise + // the action button would fail and the user would not notice something + // changing. + // + if (!!this.highlightSettingKey) { + if (profileConfig[this.highlightSettingKey] === undefined) { + this.viewSettingChange.next('all'); + } + } + + // check if we got new values for the profile or the settings. In both cases, we need to update the + // profile settings displayed as there might be new values to show. + const profileChanged = previousProfile !== this.appProfile; + const settingsChanged = allSettings !== this.allSettings; + const globalChanged = global !== prevousGlobal; + + const settingsNeedUpdate = + profileChanged || settingsChanged || globalChanged; + + // save the current global config object so we can compare for identity changes + // the next time we're executed + prevousGlobal = global; + + if (!!this.appProfile && settingsNeedUpdate) { + // filter the settings and remove all settings that are not + // profile specific (i.e. not part of the global config). Also + // update the current settings value (from the app profile) and + // the default value (from the global profile). + this.profileSettings = allSettings.map((setting) => { + setting.Value = profileConfig[setting.Key]; + setting.GlobalDefault = global[setting.Key]; + + return setting; + }); + + this.settings = this.profileSettings.filter((setting) => { + if (!(setting.Key in global)) { + return false; + } + + const isModified = setting.Value !== undefined; + if (this.viewSetting === 'all') { + return true; + } + return isModified; + }); + + this.allSettings = allSettings; + } + + this.cdr.markForCheck(); + } + ); + + this.spn.profile$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ + next: (profile) => { + this.canUseHistory = + profile?.current_plan?.feature_ids?.includes(FeatureID.History) || + false; + this.canViewBW = + profile?.current_plan?.feature_ids?.includes(FeatureID.Bandwidth) || + false; + this.canUseSPN = + profile?.current_plan?.feature_ids?.includes(FeatureID.SPN) || false; + }, + }); + } + + /** + * @private + * Retrieves debug information from the current + * profile and copies it to the clipboard + */ + copyDebugInfo() { + if (!this.appProfile) { + return; + } + + this.debugAPI + .getProfileDebugInfo(this.appProfile.Source, this.appProfile.ID) + .subscribe(async (data) => { + console.log(data); + // Copy to clip-board if supported + await this.integration.writeToClipboard(data); + this.actionIndicator.success('Copied to Clipboard'); + }); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + /** + * @private + * Delete the current profile. Requires a two-step confirmation. + */ + deleteProfile() { + if (!this.appProfile) { + return; + } + + this.dialog + .confirm({ + canCancel: true, + caption: 'Caution', + header: 'Deleting Profile ' + this.appProfile.Name, + message: + 'Do you really want to delete this profile? All settings will be lost.', + buttons: [ + { id: '', text: 'Cancel', class: 'outline' }, + { id: 'delete', class: 'danger', text: 'Yes, delete it' }, + ], + }) + .onAction('delete', () => { + this.profileService.deleteProfile(this.appProfile!).subscribe(() => { + this.router.navigate(['/app/overview']); + this.actionIndicator.success( + 'Profile Deleted', + 'Successfully deleted profile ' + this.appProfile?.Name + ); + }); + }); + } +} diff --git a/desktop/angular/src/app/pages/app-view/index.ts b/desktop/angular/src/app/pages/app-view/index.ts new file mode 100644 index 00000000..54220c42 --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/index.ts @@ -0,0 +1,3 @@ +export { AppViewComponent } from './app-view'; +export { AppOverviewComponent } from './overview'; +export { QuickSettingInternetButtonComponent } from './qs-internet'; diff --git a/desktop/angular/src/app/pages/app-view/merge-profile-dialog/merge-profile-dialog.component.html b/desktop/angular/src/app/pages/app-view/merge-profile-dialog/merge-profile-dialog.component.html new file mode 100644 index 00000000..ef6d1829 --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/merge-profile-dialog/merge-profile-dialog.component.html @@ -0,0 +1,36 @@ +
+

+ Merge Profiles +

+ + +
+ + + Please select the primary profile. All other selected profiles will be merged into the primary profile by copying metadata, fingerprints and icons into a new profile. + Only the settings of the primary profile will be kept. + + +
+ + + + + + {{ p.Name }} + + + +
+ +
+ + +
+ +
+ + +
diff --git a/desktop/angular/src/app/pages/app-view/merge-profile-dialog/merge-profile-dialog.component.ts b/desktop/angular/src/app/pages/app-view/merge-profile-dialog/merge-profile-dialog.component.ts new file mode 100644 index 00000000..d609afb1 --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/merge-profile-dialog/merge-profile-dialog.component.ts @@ -0,0 +1,62 @@ +import { AppProfile } from './../../../../../dist-lib/safing/portmaster-api/lib/app-profile.types.d'; +import { ChangeDetectionStrategy, Component, OnInit, TrackByFunction, inject } from "@angular/core"; +import { Router } from '@angular/router'; +import { PortapiService } from '@safing/portmaster-api'; +import { SFNG_DIALOG_REF, SfngDialogRef } from "@safing/ui"; +import { ActionIndicatorService } from 'src/app/shared/action-indicator'; + +@Component({ + templateUrl: './merge-profile-dialog.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styles: [ + ` + :host { + @apply flex flex-col gap-2 justify-start h-96 w-96; + } + ` + ] +}) +export class MergeProfileDialogComponent implements OnInit { + readonly dialogRef: SfngDialogRef = inject(SFNG_DIALOG_REF); + private readonly portapi = inject(PortapiService); + private readonly router = inject(Router); + private readonly uai = inject(ActionIndicatorService); + + get profiles(): AppProfile[] { + return this.dialogRef.data; + } + + primary: AppProfile | null = null; + newName = ''; + + trackProfile: TrackByFunction = (_, p) => `${p.Source}/${p.ID}` + + ngOnInit(): void { + (() => { }); + } + + mergeProfiles() { + if (!this.primary) { + return + } + + this.portapi.mergeProfiles( + this.newName, + `${this.primary.Source}/${this.primary.ID}`, + this.profiles + .filter(p => p !== this.primary) + .map(p => `${p.Source}/${p.ID}`) + ) + .subscribe({ + next: newID => { + this.router.navigate(['/app/' + newID]) + this.uai.success('Profiles Merged Successfully', 'All selected profiles have been merged') + + this.dialogRef.close() + }, + error: err => { + this.uai.error('Failed To Merge Profiles', this.uai.getErrorMessgae(err)) + } + }) + } +} diff --git a/desktop/angular/src/app/pages/app-view/overview.html b/desktop/angular/src/app/pages/app-view/overview.html new file mode 100644 index 00000000..defb9c85 --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/overview.html @@ -0,0 +1,193 @@ +
+ + +
+ +
+

+ All Apps + +

+
+ + + Create profile + Import Profile + Merge or Delete profiles + + +
+ +
+ Manage + + + + +
+
+ + + + Merge Profiles + Delete Profiles + Cancel + + + +
+ {{ selectedProfileCount}} selected + + + + +
+
+
+
+
+ +
+ +
+

Active

+
+ +
+ + +
+

Recently Edited

+
+ +
+ + +
+

All

+
+ +
+ + + +
+ + + + + + + + + + +
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+ No applications match your search term. +
diff --git a/desktop/angular/src/app/pages/app-view/overview.scss b/desktop/angular/src/app/pages/app-view/overview.scss new file mode 100644 index 00000000..87462ccb --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/overview.scss @@ -0,0 +1,54 @@ +:host { + justify-content: flex-start; +} + +.header-title { + display: flex; + width: 100%; + margin-bottom: 0.5rem; + align-items: center; + height: 3rem; + flex-shrink: 0; + + h1 { + flex-grow: unset; + } + + fa-icon[icon*="question-circle"] { + margin-left: 0.35rem; + } +} + +.scrollable { + width: auto; + flex-grow: 0; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; +} + + +.scrollable-header { + + @apply bg-background; + @apply pt-4; + @apply pb-1; + width: 100%; + position: sticky; + top: 0px; + display: flex; + + grid-column: 1 / -1; + + fa-icon[icon*="question-circle"] { + margin-left: 0.35rem; + } +} + + +.card-header { + // Card headers have top-margin by default. + // Since we're using a grid-gap anyway we need + // to clear the margin. + @apply mt-0; +} diff --git a/desktop/angular/src/app/pages/app-view/overview.ts b/desktop/angular/src/app/pages/app-view/overview.ts new file mode 100644 index 00000000..3c995621 --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/overview.ts @@ -0,0 +1,305 @@ +import { + ChangeDetectorRef, + Component, + OnDestroy, + OnInit, + TrackByFunction, +} from '@angular/core'; +import { + AppProfile, + AppProfileService, + Netquery, + trackById, +} from '@safing/portmaster-api'; +import { SfngDialogService } from '@safing/ui'; +import { BehaviorSubject, Subscription, combineLatest, forkJoin } from 'rxjs'; +import { debounceTime, filter, startWith } from 'rxjs/operators'; +import { + fadeInAnimation, + fadeInListAnimation, + moveInOutListAnimation, +} from 'src/app/shared/animations'; +import { FuzzySearchService } from 'src/app/shared/fuzzySearch'; +import { EditProfileDialog } from './../../shared/edit-profile-dialog/edit-profile-dialog'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { MergeProfileDialogComponent } from './merge-profile-dialog/merge-profile-dialog.component'; +import { ActionIndicatorService } from 'src/app/shared/action-indicator'; +import { Router } from '@angular/router'; +import { + ImportConfig, + ImportDialogComponent, +} from 'src/app/shared/config/import-dialog/import-dialog.component'; + +interface LocalAppProfile extends AppProfile { + hasConfigChanges: boolean; + selected: boolean; +} + +@Component({ + selector: 'app-settings-overview', + templateUrl: './overview.html', + styleUrls: ['../page.scss', './overview.scss'], + animations: [fadeInAnimation, fadeInListAnimation, moveInOutListAnimation], +}) +export class AppOverviewComponent implements OnInit, OnDestroy { + private subscription = Subscription.EMPTY; + + /** Whether or not we are currently loading */ + loading = true; + + /** All application profiles that are actually running */ + runningProfiles: LocalAppProfile[] = []; + + /** All application profiles that have been edited recently */ + recentlyEdited: LocalAppProfile[] = []; + + /** All application profiles */ + profiles: LocalAppProfile[] = []; + + /** The current search term */ + searchTerm: string = ''; + + /** total number of profiles */ + total: number = 0; + + /** Whether or not we are in profile-selection mode */ + set selectMode(v: any) { + this._selectMode = coerceBooleanProperty(v); + + // reset all previous profile selections + if (!this._selectMode) { + this.profiles.forEach((profile) => (profile.selected = false)); + } + } + get selectMode() { + return this._selectMode; + } + private _selectMode = false; + + get selectedProfileCount() { + return this.profiles.reduce( + (sum, profile) => (profile.selected ? sum + 1 : sum), + 0 + ); + } + + /** Observable emitting the search term */ + private onSearch = new BehaviorSubject(''); + + /** TrackBy function for the profiles. */ + trackProfile: TrackByFunction = trackById; + + constructor( + private profileService: AppProfileService, + private changeDetector: ChangeDetectorRef, + private searchService: FuzzySearchService, + private netquery: Netquery, + private dialog: SfngDialogService, + private actionIndicator: ActionIndicatorService, + private router: Router + ) { } + + handleProfileClick(profile: LocalAppProfile, event: MouseEvent) { + if (event.shiftKey) { + // stay on the same page as clicking the app actually triggers + // a navigation before this handler is executed. + this.router.navigate(['/app/overview']); + + this.selectMode = true; + + event.preventDefault(); + event.stopImmediatePropagation(); + event.stopPropagation(); + } + + if (this.selectMode) { + profile.selected = !profile.selected; + } + + if (event.shiftKey && this.selectedProfileCount === 0) { + this.selectMode = false; + } + } + + importProfile() { + const importConfig: ImportConfig = { + type: 'profile', + key: '', + }; + + this.dialog.create(ImportDialogComponent, { + data: importConfig, + autoclose: false, + backdrop: 'light', + }); + } + + openMergeDialog() { + this.dialog.create(MergeProfileDialogComponent, { + autoclose: true, + backdrop: 'light', + data: this.profiles.filter((p) => p.selected), + }); + + this.selectMode = false; + } + + deleteSelectedProfiles() { + this.dialog + .confirm({ + header: 'Confirm Profile Deletion', + message: `Are you sure you want to delete all ${this.selectedProfileCount} selected profiles?`, + caption: 'Attention', + buttons: [ + { + id: 'no', + text: 'Cancel', + class: 'outline', + }, + { + id: 'yes', + text: 'Delete', + class: 'danger', + }, + ], + }) + .onAction('yes', () => { + forkJoin( + this.profiles + .filter((profile) => profile.selected) + .map((p) => this.profileService.deleteProfile(p)) + ).subscribe({ + next: () => { + this.actionIndicator.success( + 'Selected Profiles Delete', + 'All selected profiles have been deleted' + ); + }, + error: (err) => { + this.actionIndicator.error( + 'Failed To Delete Profiles', + `An error occured while deleting some profiles: ${this.actionIndicator.getErrorMessgae( + err + )}` + ); + }, + }); + }) + .onClose.subscribe(() => (this.selectMode = false)); + } + + ngOnInit() { + // watch all profiles and re-emit (debounced) when the user + // enters or chanages the search-text. + this.subscription = combineLatest([ + this.profileService.watchProfiles(), + this.onSearch.pipe(debounceTime(100), startWith('')), + this.netquery.getActiveProfileIDs().pipe(startWith([] as string[])), + ]).subscribe(([profiles, searchTerm, activeProfiles]) => { + this.loading = false; + + // find all profiles that match the search term. For searchTerm="" thsi + // will return all profiles. + const filtered = this.searchService.searchList(profiles, searchTerm, { + ignoreLocation: true, + ignoreFieldNorm: true, + threshold: 0.1, + minMatchCharLength: 3, + keys: ['Name', 'PresentationPath'], + }); + + // create a lookup map of all profiles we already loaded so we don't loose + // selection state when a profile has been updated. + const oldProfiles = new Map( + this.profiles.map((profile) => [ + `${profile.Source}/${profile.ID}`, + profile, + ]) + ); + + // Prepare new, empty lists for our groups + this.profiles = []; + this.runningProfiles = []; + this.recentlyEdited = []; + + // calcualte the threshold for "recently-used" (1 week). + const recentlyUsedThreshold = + new Date().valueOf() / 1000 - 60 * 60 * 24 * 7; + + // flatten the filtered profiles, sort them by name and group them into + // our "app-groups" (active, recentlyUsed, others) + this.total = filtered.length; + filtered + .map((item) => item.item) + .sort((a, b) => { + const aName = a.Name.toLocaleLowerCase(); + const bName = b.Name.toLocaleLowerCase(); + + if (aName > bName) { + return 1; + } + + if (aName < bName) { + return -1; + } + + return 0; + }) + .forEach((profile) => { + const local: LocalAppProfile = { + ...profile, + hasConfigChanges: + profile.LastEdited > 0 && Object.keys(profile.Config || {}).length > 0, + selected: + oldProfiles.get(`${profile.Source}/${profile.ID}`)?.selected || + false, + }; + + if (activeProfiles.includes(profile.Source + '/' + profile.ID)) { + this.runningProfiles.push(local); + } else if (profile.LastEdited >= recentlyUsedThreshold) { + this.recentlyEdited.push(local); + } + + // we always add the profile to "All Apps" + this.profiles.push(local); + }); + + this.changeDetector.markForCheck(); + }); + } + + /** + * @private + * + * Used as an ngModelChange callback on the search-input. + * + * @param term The search term entered by the user + */ + searchApps(term: string) { + this.searchTerm = term; + this.onSearch.next(term); + } + + /** + * @private + * + * Opens the create profile dialog + */ + createProfile() { + const ref = this.dialog.create(EditProfileDialog, { + backdrop: true, + autoclose: false, + }); + + ref.onClose.pipe(filter((action) => action === 'saved')).subscribe(() => { + // reset the search and reload to make sure the new + // profile shows up + this.searchApps(''); + }); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } +} diff --git a/desktop/angular/src/app/pages/app-view/qs-history/qs-history.component.html b/desktop/angular/src/app/pages/app-view/qs-history/qs-history.component.html new file mode 100644 index 00000000..77c34199 --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/qs-history/qs-history.component.html @@ -0,0 +1,12 @@ +
+ + Keep History + + + + Get Plus + + + +
diff --git a/desktop/angular/.gitkeep b/desktop/angular/src/app/pages/app-view/qs-history/qs-history.component.scss similarity index 100% rename from desktop/angular/.gitkeep rename to desktop/angular/src/app/pages/app-view/qs-history/qs-history.component.scss diff --git a/desktop/angular/src/app/pages/app-view/qs-history/qs-history.component.ts b/desktop/angular/src/app/pages/app-view/qs-history/qs-history.component.ts new file mode 100644 index 00000000..24e6296a --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/qs-history/qs-history.component.ts @@ -0,0 +1,67 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, + inject, +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + BoolSetting, + FeatureID, + SPNService, + Setting, + getActualValue, +} from '@safing/portmaster-api'; +import { BehaviorSubject, Observable, map } from 'rxjs'; +import { share } from 'rxjs/operators'; +import { SaveSettingEvent } from 'src/app/shared/config'; + +@Component({ + selector: 'app-qs-history', + templateUrl: './qs-history.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class QsHistoryComponent implements OnChanges { + currentValue = false; + historyFeatureAllowed: Observable = inject(SPNService).profile$.pipe( + takeUntilDestroyed(), + map((profile) => { + return ( + profile?.current_plan?.feature_ids?.includes(FeatureID.History) || false + ); + }), + share({ connector: () => new BehaviorSubject(false) }) + ); + + @Input() + canUse: boolean = true; + + @Input() + settings: Setting[] = []; + + @Output() + save = new EventEmitter>(); + + ngOnChanges(changes: SimpleChanges): void { + if ('settings' in changes) { + const historySetting = this.settings.find( + (s) => s.Key === 'history/enable' + ) as BoolSetting | undefined; + if (historySetting) { + this.currentValue = getActualValue(historySetting); + } + } + } + + updateHistoryEnabled(enabled: boolean) { + this.save.next({ + isDefault: false, + key: 'history/enable', + value: enabled, + }); + } +} diff --git a/desktop/angular/src/app/pages/app-view/qs-internet/index.ts b/desktop/angular/src/app/pages/app-view/qs-internet/index.ts new file mode 100644 index 00000000..7fd0d0c4 --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/qs-internet/index.ts @@ -0,0 +1 @@ +export * from './qs-internet'; diff --git a/desktop/angular/src/app/pages/app-view/qs-internet/qs-internet.html b/desktop/angular/src/app/pages/app-view/qs-internet/qs-internet.html new file mode 100644 index 00000000..cf87f254 --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/qs-internet/qs-internet.html @@ -0,0 +1,30 @@ +
+ + Block Connections + + + + + + + + Prompting + +
+ + + The following enabled settings may interfere: +
    + +
  • + {{ setting.Name }} +
  • +
    +
+
diff --git a/desktop/angular/src/app/pages/app-view/qs-internet/qs-internet.ts b/desktop/angular/src/app/pages/app-view/qs-internet/qs-internet.ts new file mode 100644 index 00000000..7b606660 --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/qs-internet/qs-internet.ts @@ -0,0 +1,79 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from "@angular/core"; +import { Setting, StringSetting, getActualValue } from "@safing/portmaster-api"; +import { SaveSettingEvent } from "src/app/shared/config/generic-setting/generic-setting"; + +const interferingSettings = { + 'permit': [ + 'filter/blockInternet', + 'filter/blockLAN', + 'filter/blockLocal', + 'filter/blockP2P', + 'filter/blockInbound', + 'filter/endpoints', + ], + 'block': [ + 'filter/endpoints', + ], +} + +@Component({ + selector: 'app-qs-internet', + templateUrl: './qs-internet.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class QuickSettingInternetButtonComponent implements OnChanges { + @Input() + settings: Setting[] = []; + + @Output() + save = new EventEmitter(); + + currentValue = '' + + interferingSettings: Setting[] = []; + + ngOnChanges(changes: SimpleChanges): void { + if ('settings' in changes) { + this.currentValue = ''; + const defaultActionSetting = this.settings.find(s => s.Key == 'filter/defaultAction') as (StringSetting | undefined); + if (!!defaultActionSetting) { + this.currentValue = getActualValue(defaultActionSetting); + this.updateInterfering(); + } + } + } + + updateUseInternet(blocked: boolean) { + const newValue = blocked ? 'block' : 'permit'; + this.save.next({ + isDefault: false, + key: 'filter/defaultAction', + value: newValue, + }) + } + + private updateInterfering() { + this.interferingSettings = []; + if (this.currentValue !== 'permit' && this.currentValue !== 'block') { + return; + } + + // create a lookup map for setting key to setting + const lm = new Map(); + this.settings.forEach(s => lm.set(s.Key, s)) + + this.interferingSettings = interferingSettings[this.currentValue] + .map(key => lm.get(key)) + .filter(setting => { + if (!setting) { + return false; + } + const value = getActualValue(setting); + if (Array.isArray(value)) { + return value.length > 0; + } + + return !!value; + }) as Setting[]; + } +} diff --git a/desktop/angular/src/app/pages/app-view/qs-select-exit/index.ts b/desktop/angular/src/app/pages/app-view/qs-select-exit/index.ts new file mode 100644 index 00000000..56c7267e --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/qs-select-exit/index.ts @@ -0,0 +1 @@ +export * from './qs-select-exit'; diff --git a/desktop/angular/src/app/pages/app-view/qs-select-exit/qs-select-exit.html b/desktop/angular/src/app/pages/app-view/qs-select-exit/qs-select-exit.html new file mode 100644 index 00000000..66f9fa13 --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/qs-select-exit/qs-select-exit.html @@ -0,0 +1,39 @@ +
+ + SPN Exit + + + + Get Pro + + + + + Automatic + + + + + + + + + + {{ option.Name }} + + + + + + + + + + Disabled + + +
diff --git a/desktop/angular/src/app/pages/app-view/qs-select-exit/qs-select-exit.scss b/desktop/angular/src/app/pages/app-view/qs-select-exit/qs-select-exit.scss new file mode 100644 index 00000000..e69de29b diff --git a/desktop/angular/src/app/pages/app-view/qs-select-exit/qs-select-exit.ts b/desktop/angular/src/app/pages/app-view/qs-select-exit/qs-select-exit.ts new file mode 100644 index 00000000..698607b6 --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/qs-select-exit/qs-select-exit.ts @@ -0,0 +1,128 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + inject, +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + BoolSetting, + StringArraySetting, + CountrySelectionQuickSetting, + ConfigService, + Setting, + getActualValue, +} from '@safing/portmaster-api'; +import { SaveSettingEvent } from 'src/app/shared/config/generic-setting/generic-setting'; + +@Component({ + selector: 'app-qs-select-exit', + templateUrl: './qs-select-exit.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class QuickSettingSelectExitButtonComponent + implements OnInit, OnChanges +{ + private destroyRef = inject(DestroyRef); + + @Input() + canUse: boolean = true; + + @Input() + settings: Setting[] = []; + + @Output() + save = new EventEmitter(); + + spnEnabled: boolean | null = null; + exitRuleSetting: StringArraySetting | null = null; + + selectedExitRules: string | undefined = undefined; + availableExitRules: CountrySelectionQuickSetting[] | null = null; + + constructor( + private configService: ConfigService, + private cdr: ChangeDetectorRef + ) {} + + updateExitRules(newExitRules: string) { + this.selectedExitRules = newExitRules; + + let newConfigValue: string[] = []; + if (!!newExitRules) { + newConfigValue = newExitRules.split(','); + } + + this.save.next({ + isDefault: false, + key: 'spn/exitHubPolicy', + value: newConfigValue, + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if ('settings' in changes) { + this.exitRuleSetting = null; + this.selectedExitRules = undefined; + + const exitRuleSetting = this.settings.find( + (s) => s.Key == 'spn/exitHubPolicy' + ) as StringArraySetting | undefined; + if (exitRuleSetting) { + this.exitRuleSetting = exitRuleSetting; + this.updateOptions(); + } + } + } + + ngOnInit() { + this.configService + .watch('spn/enable') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((value) => { + this.spnEnabled = value; + this.updateOptions(); + }); + } + + private updateOptions() { + if (!this.exitRuleSetting) { + this.selectedExitRules = undefined; + this.availableExitRules = null; + return; + } + + if (!!this.exitRuleSetting.Value && this.exitRuleSetting.Value.length > 0) { + this.selectedExitRules = this.exitRuleSetting.Value.join(','); + } + this.availableExitRules = this.getQuickSettings(); + + this.cdr.markForCheck(); + } + + private getQuickSettings(): CountrySelectionQuickSetting[] { + if (!this.exitRuleSetting) { + return []; + } + + let val = this.exitRuleSetting.Annotations[ + 'safing/portbase:ui:quick-setting' + ] as CountrySelectionQuickSetting[]; + if (val === undefined) { + return []; + } + + if (!Array.isArray(val)) { + return []; + } + + return val; + } +} diff --git a/desktop/angular/src/app/pages/app-view/qs-use-spn/index.ts b/desktop/angular/src/app/pages/app-view/qs-use-spn/index.ts new file mode 100644 index 00000000..aba9748a --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/qs-use-spn/index.ts @@ -0,0 +1 @@ +export * from './qs-use-spn'; diff --git a/desktop/angular/src/app/pages/app-view/qs-use-spn/qs-use-spn.html b/desktop/angular/src/app/pages/app-view/qs-use-spn/qs-use-spn.html new file mode 100644 index 00000000..58ceb09d --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/qs-use-spn/qs-use-spn.html @@ -0,0 +1,42 @@ +
+ + Use SPN + + + + Get Pro + + + + + + + Disabled + + + + + + + + +
+ + + The following enabled settings may interfere: +
    + +
  • + {{ setting.Name }} +
  • +
    +
+
diff --git a/desktop/angular/src/app/pages/app-view/qs-use-spn/qs-use-spn.ts b/desktop/angular/src/app/pages/app-view/qs-use-spn/qs-use-spn.ts new file mode 100644 index 00000000..f4e5bb2e --- /dev/null +++ b/desktop/angular/src/app/pages/app-view/qs-use-spn/qs-use-spn.ts @@ -0,0 +1,97 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { BoolSetting, ConfigService, Setting, getActualValue } from "@safing/portmaster-api"; +import { SaveSettingEvent } from "src/app/shared/config/generic-setting/generic-setting"; + +const interferingSettingsWhenOn = [ + 'spn/usagePolicy' +] + +@Component({ + selector: 'app-qs-use-spn', + templateUrl: './qs-use-spn.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class QuickSettingUseSPNButtonComponent implements OnInit, OnChanges { + private destroyRef = inject(DestroyRef); + + @Input() + canUse: boolean = true; + + @Input() + settings: Setting[] = []; + + @Output() + save = new EventEmitter(); + + currentValue = false + + interferingSettings: Setting[] = []; + + /* Whether or not the SPN is currently disabled. null means we don't know yet ... */ + spnDisabled: boolean | null = null; + + constructor( + private configService: ConfigService, + private cdr: ChangeDetectorRef + ) { } + + ngOnChanges(changes: SimpleChanges): void { + if ('settings' in changes) { + this.currentValue = false; + + const useSpnSetting = this.settings.find(s => s.Key === 'spn/use') as (BoolSetting | undefined); + if (!!useSpnSetting) { + this.currentValue = getActualValue(useSpnSetting); + this.updateInterfering(); + } + } + } + + updateUseSpn(allowed: boolean) { + this.save.next({ + isDefault: false, + key: 'spn/use', + value: allowed, + }) + } + + ngOnInit() { + this.configService.watch('spn/enable') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => { + this.spnDisabled = !value; + this.cdr.markForCheck(); + this.updateInterfering(); + }) + } + + private updateInterfering() { + this.interferingSettings = []; + + // only "enabled" state has interfering settings + // only show if we already know if the SPN module is enabled + if (!this.currentValue || this.spnDisabled !== false) { + return + } + + // create a lookup map for setting key to setting + const lm = new Map(); + this.settings.forEach(s => lm.set(s.Key, s)) + + + this.interferingSettings = interferingSettingsWhenOn + .map(key => lm.get(key)) + .filter(setting => { + if (!setting) { + return false; + } + const value = getActualValue(setting); + if (Array.isArray(value)) { + return value.length > 0; + } + + return !!value; + }) as Setting[]; + } +} diff --git a/desktop/angular/src/app/pages/dashboard/dashboard-widget/dashboard-widget.component.html b/desktop/angular/src/app/pages/dashboard/dashboard-widget/dashboard-widget.component.html new file mode 100644 index 00000000..65fd80cf --- /dev/null +++ b/desktop/angular/src/app/pages/dashboard/dashboard-widget/dashboard-widget.component.html @@ -0,0 +1,14 @@ + + + diff --git a/desktop/angular/src/app/pages/dashboard/dashboard-widget/dashboard-widget.component.ts b/desktop/angular/src/app/pages/dashboard/dashboard-widget/dashboard-widget.component.ts new file mode 100644 index 00000000..35d2668a --- /dev/null +++ b/desktop/angular/src/app/pages/dashboard/dashboard-widget/dashboard-widget.component.ts @@ -0,0 +1,30 @@ +import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { ChangeDetectionStrategy, Component, Input } from "@angular/core"; + +@Component({ + selector: 'app-dashboard-widget', + templateUrl: './dashboard-widget.component.html', + styles: [ + ` + :host { + @apply bg-gray-200 p-4 self-stretch rounded-md flex flex-col gap-2; + } + + label { + @apply text-xs uppercase text-secondary font-light flex flex-row items-center gap-2 pb-2; + } + ` + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DashboardWidgetComponent { + @Input() + set beta(v: any) { + this._beta = coerceBooleanProperty(v) + } + get beta() { return this._beta } + private _beta = false; + + @Input() + label: string = ''; +} diff --git a/desktop/angular/src/app/pages/dashboard/dashboard.component.html b/desktop/angular/src/app/pages/dashboard/dashboard.component.html new file mode 100644 index 00000000..aaf3077c --- /dev/null +++ b/desktop/angular/src/app/pages/dashboard/dashboard.component.html @@ -0,0 +1,281 @@ +
+ + + + +
+ + +
+
+ + + + +
+
+ + {{ blockedConnections }} +
+ +
+ + {{ activeConnections }} +
+ +
+ + {{ activeProfiles }} +
+ +
+ + + {{ dataIncoming | bytes }} + + + Available in
Portmaster Plus +
+
+ +
+ + + {{ dataOutgoing | bytes }} + + + Available in
Portmaster Plus +
+
+ +
+ + {{ activeIdentities }} + + Available in
Portmaster Pro +
+
+
+
+ + +
+
+ + +
+
+ + + +
+
+
+ + +
+
    +
  • +
    + + {{ countryNames[country.key] || country.key || 'N/A' }} +
    + {{ country.value }} +
  • +
+
+
+ + +
+ + + + + + + No applications have been blocked in the last 10 minutes. + + + +
    + +
  • +
    + + {{ profile.Name }} +
    + {{ entry.count }} +
  • +
    +
+
+
+ + + + + + + + + + Available in Portmaster Plus + + + + + + + + Available in Portmaster Plus + + + + + +
+ News is only available if intel data updates are enabled + +
+ +
+ Just a second, we're loading the latest news... +
+ + + + +
+ +

+ {{ card.title }} + +

+
+ + + +
+
+
+
+ {{ progress.percent }}% +
+
+
+ + + +
+
+
+ +
+ +
+
+ +
+
diff --git a/desktop/angular/src/app/pages/dashboard/dashboard.component.scss b/desktop/angular/src/app/pages/dashboard/dashboard.component.scss new file mode 100644 index 00000000..ba37e527 --- /dev/null +++ b/desktop/angular/src/app/pages/dashboard/dashboard.component.scss @@ -0,0 +1,166 @@ +:host { + @apply flex flex-row w-full gap-3 p-4 overflow-auto; +} + +.dashboard-grid { + @apply grid gap-4; + + align-items: stretch; + justify-items: stretch; + grid-template-columns: 1fr 1fr 1fr 1fr; + grid-template-areas: + "header header header header" + "feature feature feature feature" + "feature feature feature feature" + "stats stats news news" + "stats stats news news" + "charts charts charts charts" + "charts charts charts charts" + "blocked blocked countries countries" + "map map map map" + "bwvis-bar bwvis-bar bwvis-line bwvis-line"; +} + +:host-context(.min-width-1024px) { + .dashboard-grid { + grid-template-areas: + "header header header header" + "feature feature feature news" + "feature feature feature news" + "stats stats stats news" + "stats stats stats news" + "charts charts charts charts" + "countries countries map map" + "blocked blocked map map" + "bwvis-bar bwvis-bar bwvis-line bwvis-line"; + } +} + +#header { + grid-area: header; +} + +#features { + grid-area: feature; +} + +#stats { + grid-area: stats; +} + +#charts { + grid-area: charts; +} + +#countries { + grid-area: countries; +} + +#blocked { + grid-area: blocked; +} + +#connmap { + grid-area: map; +} + +#bwvis-bar { + grid-area: bwvis-bar; +} + +#bwvis-line { + grid-area: bwvis-line; +} + +#news { + grid-area: news; +} + +.auto-grid-3 { + @apply grid gap-4; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); +} + +.auto-grid-4 { + @apply grid gap-4; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); +} + +app-dashboard-widget { + label { + @apply text-xs uppercase text-secondary font-light flex flex-row items-center gap-2 pb-2; + } + + .feature-card-list { + @apply grid gap-3 w-full; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + } + + .mini-stats-list { + @apply grid gap-3 w-full; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + } + + &#news { + + h1 { + @apply text-base; + @apply font-light; + } + + ::ng-deep markdown { + @apply font-light; + + a { + @apply underline text-blue; + } + + strong { + @apply font-medium; + } + } + } + +} + +::ng-deep #dashboard-map { + #world-group { + --map-bg: #111112; + --map-country-active: #424141; + --map-country-inactive: #2a2a2a; + --map-country-border-width: 1px; + --map-country-border-color: #1e1e1e; + --map-country-border-color-selected: #858585; + --map-country-blocked-primary: #858585; + --map-country-blocked-secondary: #402323; + + path { + fill: var(--map-country-active); + stroke: var(--map-bg); + stroke-width: var(--map-country-border-width); + stroke-linejoin: round; + } + + path.active { + color: #1d3c24; + fill: currentColor; + } + + path.hover { + color: #4fae4f; + fill: currentColor; + } + } +} + +.mini-stat { + @apply flex flex-col items-center justify-center py-3 px-2 bg-gray-300 rounded shadow; + + label { + @apply font-light uppercase text-xxs text-secondary -mb-2; + } + + span { + @apply text-xl text-blue; + } +} diff --git a/desktop/angular/src/app/pages/dashboard/dashboard.component.ts b/desktop/angular/src/app/pages/dashboard/dashboard.component.ts new file mode 100644 index 00000000..a07893aa --- /dev/null +++ b/desktop/angular/src/app/pages/dashboard/dashboard.component.ts @@ -0,0 +1,481 @@ +import { KeyValue } from "@angular/common"; +import { AfterContentInit, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, ElementRef, OnInit, QueryList, TrackByFunction, ViewChild, ViewChildren, forwardRef, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { AppProfileService, BandwidthChartResult, ChartResult, Database, FeatureID, Netquery, PortapiService, SPNService, UserProfile, Verdict } from "@safing/portmaster-api"; +import { SfngDialogService, SfngTabGroupComponent } from "@safing/ui"; +import { Observable, catchError, filter, interval, map, repeat, retry, startWith, throwError } from "rxjs"; +import { ActionIndicatorService } from 'src/app/shared/action-indicator'; +import { DefaultBandwidthChartConfig, SfngNetqueryLineChartComponent } from "src/app/shared/netquery/line-chart/line-chart"; +import { SPNAccountDetailsComponent } from "src/app/shared/spn-account-details"; +import { MAP_HANDLER, MapRef } from "../spn/map-renderer"; +import { CircularBarChartConfig, splitQueryResult } from "src/app/shared/netquery/circular-bar-chart/circular-bar-chart.component"; +import { BytesPipe } from "src/app/shared/pipes/bytes.pipe"; +import { HttpErrorResponse } from "@angular/common/http"; + +interface BlockedProfile { + profileID: string; + count: number; +} + +interface BandwidthBarData { + profile: string; + profile_name: string; + series: 'sent' | 'received'; + value: number; + sent: number; + received: number; +} + +interface NewsCard { + title: string; + body: string; + url?: string; + footer?: string; + progress?: { + percent: number; + style: string; + } +} + +interface News { + cards: NewsCard[]; +} + +const newsResourceIdentifier = "all/intel/portmaster/news.yaml" + +@Component({ + selector: 'app-dashboard', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./dashboard.component.scss'], + templateUrl: './dashboard.component.html', + providers: [ + { provide: MAP_HANDLER, useExisting: forwardRef(() => DashboardPageComponent), multi: true }, + ] +}) +export class DashboardPageComponent implements OnInit, AfterViewInit { + @ViewChildren(SfngNetqueryLineChartComponent) + lineCharts!: QueryList; + + @ViewChild(SfngTabGroupComponent) + carouselTabGroup?: SfngTabGroupComponent; + + private readonly destroyRef = inject(DestroyRef); + private readonly netquery = inject(Netquery); + private readonly spn = inject(SPNService); + private readonly actionIndicator = inject(ActionIndicatorService); + private readonly cdr = inject(ChangeDetectorRef); + private readonly dialog = inject(SfngDialogService); + private readonly portapi = inject(PortapiService) + + resizeObserver!: ResizeObserver; + + blockedProfiles: BlockedProfile[] = [] + + connectionsPerCountry: { + [country: string]: number + } = {}; + + get countryNames(): { [country: string]: string } { + return this.mapRef?.countryNames || {}; + } + + bandwidthLineChart: BandwidthChartResult[] = []; + + bandwidthBarData: BandwidthBarData[] = []; + + readonly bandwidthBarConfig: CircularBarChartConfig = { + stack: 'profile_name', + seriesKey: 'series', + seriesLabel: d => { + if (d === 'sent') { + return 'Bytes Sent' + } + return 'Bytes Received' + }, + value: 'value', + ticks: 3, + colorAsClass: true, + series: { + 'sent': { + color: 'text-deepPurple-500 text-opacity-50', + }, + 'received': { + color: 'text-cyan-800 text-opacity-50', + } + }, + formatTick: (tick: number) => { + return new BytesPipe().transform(tick, '1.0-0') + }, + formatValue: (stack, series, value, data) => { + const bytes = new BytesPipe().transform + return `${stack}\nSent: ${bytes(data?.sent)}\nReceived: ${bytes(data?.received)}` + }, + formatStack: (sel, data) => { + const bytes = new BytesPipe().transform + + return sel + .call(sel => { + sel.append("text") + .attr("dy", "0") + .attr("y", "0") + .text(d => d) + }) + .call(sel => { + sel.append("text") + .attr("y", 0) + .attr("dy", "0.8rem") + .style("font-size", "0.6rem") + .text(d => { + const first = data.find(result => result.profile_name === d); + return `${bytes(first?.sent)} / ${bytes(first?.received)}` + }) + }) + } + } + + bwChartConfig = DefaultBandwidthChartConfig; + + activeConnections: number = 0; + blockedConnections: number = 0; + activeProfiles: number = 0; + activeIdentities = 0; + dataIncoming = 0; + dataOutgoing = 0; + connectionChart: ChartResult[] = []; + tunneldConnectionChart: ChartResult[] = []; + + countriesPerProfile: { [profile: string]: string[] } = {} + + profile: UserProfile | null = null; + + featureBw = false; + featureSPN = false; + + hoveredCard: NewsCard | null = null; + + features$ = this.spn.watchEnabledFeatures() + .pipe(takeUntilDestroyed()); + + trackCountry: TrackByFunction> = (_, ctr) => ctr.key; + trackApp: TrackByFunction = (_, bp) => bp.profileID; + + data: any; + + news?: News | 'pending' = 'pending'; + + private mapRef: MapRef | null = null; + + registerMap(ref: MapRef): void { + this.mapRef = ref; + + this.mapRef.onMapReady(() => { + this.updateMapCountries(); + }) + } + + private updateMapCountries() { + // this check is basically to make typescript happy ... + if (!this.mapRef) { + return; + } + + this.mapRef.worldGroup + .selectAll('path') + .classed('active', (d: any) => { + return !!this.connectionsPerCountry[d.properties.iso_a2]; + }); + } + + unregisterMap(ref: MapRef): void { + this.mapRef = null; + } + + onCarouselTabHover(card: NewsCard | null) { + this.hoveredCard = card; + } + + openAccountDetails() { + this.dialog.create(SPNAccountDetailsComponent, { + autoclose: true, + backdrop: 'light' + }) + } + + onCountryHover(code: string | null) { + if (!this.mapRef) { + return + } + + this.mapRef.worldGroup + .selectAll('path') + .classed('hover', (d: any) => { + return (d.properties.iso_a2 === code); + }); + } + + onProfileHover(profile: string | null) { + if (!this.mapRef) { + return + } + + this.mapRef.worldGroup + .selectAll('path') + .classed('hover', (d: any) => { + if (!profile) { + return false; + } + + return this.countriesPerProfile[profile]?.includes(d.properties.iso_a2); + }); + } + + ngAfterViewInit(): void { + interval(15000) + .pipe( + takeUntilDestroyed(this.destroyRef), + startWith(-1), + filter(() => this.hoveredCard === null) + ) + .subscribe(() => { + if (!this.carouselTabGroup) { + return + } + + let next = this.carouselTabGroup.activeTabIndex + 1 + if (next >= this.carouselTabGroup.tabs!.length) { + next = 0 + } + + this.carouselTabGroup.activateTab(next, "left") + }) + } + + async ngOnInit() { + this.portapi.getResource(newsResourceIdentifier) + .pipe( + repeat({ delay: 60000 }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe({ + next: response => { + this.news = response; + this.cdr.markForCheck(); + }, + error: () => { + this.news = undefined; + this.cdr.markForCheck(); + } + }); + + this.netquery + .batch({ + bwBarChart: { + query: { + internal: { $eq: false }, + }, + select: [ + 'profile', + 'profile_name', + { + $sum: { + field: 'bytes_sent', + as: 'sent' + } + }, + { + $sum: { + field: 'bytes_received', + as: 'received' + } + }, + ], + groupBy: ['profile', 'profile_name'], + }, + + profileCount: { + select: [ + 'profile', + { + $count: { + field: '*', + as: 'totalCount' + } + } + ], + query: { + verdict: { $in: [Verdict.Block, Verdict.Drop] } + }, + groupBy: ['profile'], + databases: [Database.Live] + }, + + countryStats: { + select: [ + 'country', + { $count: { field: '*', as: 'totalCount' } }, + { $sum: { field: 'bytes_sent', as: 'bwout' } }, + { $sum: { field: 'bytes_received', as: 'bwin' } }, + ], + query: { + allowed: { $eq: true }, + }, + groupBy: ['country'], + databases: [Database.Live] + }, + + perCountryConns: { + select: ['profile', 'country', 'active', { $count: { field: '*', as: 'totalCount' } }], + query: { + allowed: { $eq: true }, + }, + groupBy: ['profile', 'country', 'active'], + databases: [Database.Live], + }, + + exitNodes: { + query: { tunneled: { $eq: true }, exit_node: { $ne: "" } }, + groupBy: ['exit_node'], + select: [ + 'exit_node', + { $count: { field: '*', as: 'totalCount' } } + ], + databases: [Database.Live], + } + }) + .pipe( + repeat({ delay: 10000 }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(response => { + // bandwidth bar chart + const barChartData = response.bwBarChart + .filter(value => (value.sent + value.received) > 0) + .sort((a, b) => (b.sent + b.received) - (a.sent + a.received)) + .slice(0, 10); + this.bandwidthBarData = splitQueryResult(barChartData, ['sent', 'received']) as BandwidthBarData[] + + // profileCount + this.blockedConnections = 0; + this.blockedProfiles = []; + + response.profileCount?.forEach(row => { + this.blockedConnections += row.totalCount; + this.blockedProfiles.push({ + profileID: row.profile!, + count: row.totalCount + }) + }); + + // countryStats + this.connectionsPerCountry = {}; + this.dataIncoming = 0; + this.dataOutgoing = 0; + + response.countryStats?.forEach(row => { + this.dataIncoming += row.bwin; + this.dataOutgoing += row.bwout; + + if (row.country === '') { + return + } + + this.connectionsPerCountry[row.country!] = row.totalCount || 0; + }) + + this.updateMapCountries() + + // perCountryConns + let profiles = new Set(); + + this.activeConnections = 0; + this.countriesPerProfile = {}; + + response.perCountryConns?.forEach(row => { + profiles.add(row.profile!); + + if (row.active) { + this.activeConnections += row.totalCount; + } + + const arr = (this.countriesPerProfile[row.profile!] || []); + arr.push(row.country!) + this.countriesPerProfile[row.profile!] = arr; + }); + + this.activeProfiles = profiles.size; + + // exitNodes + this.activeIdentities = response.exitNodes?.length || 0; + this.cdr.markForCheck(); + }) + + + // Charts + + this.netquery + .activeConnectionChart({}) + .pipe( + repeat({ delay: 10000 }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(result => { + this.connectionChart = result; + this.cdr.markForCheck(); + }) + + this.netquery + .bandwidthChart({}, undefined, 60) + .pipe( + repeat({ delay: 10000 }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(bw => { + this.bandwidthLineChart = bw; + this.cdr.markForCheck(); + }) + + this.netquery + .activeConnectionChart({ tunneled: { $eq: true } }) + .pipe( + repeat({ delay: 10000 }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(result => { + this.tunneldConnectionChart = result; + this.cdr.markForCheck(); + }) + + // SPN profile and enabled/allowed features + + this.spn + .profile$ + .pipe( + takeUntilDestroyed(this.destroyRef) + ) + .subscribe({ + next: (profile) => { + this.profile = profile || null; + this.featureBw = profile?.current_plan?.feature_ids?.includes(FeatureID.Bandwidth) || false; + this.featureSPN = profile?.current_plan?.feature_ids?.includes(FeatureID.SPN) || false; + + // force a full change-detection cylce now! + this.cdr.detectChanges() + + // force re-draw of the charts after change-detection because the + // width may change now. + this.lineCharts?.forEach(chart => chart.redraw()) + + this.cdr.markForCheck(); + }, + }) + } + + /** Logs the user out of the SPN completely by purgin the user profile from the local storage */ + logoutCompletely(_: Event) { + this.spn.logout(true) + .subscribe(this.actionIndicator.httpObserver( + 'Logout', + 'You have been logged out of the SPN completely.' + )) + } +} diff --git a/desktop/angular/src/app/pages/dashboard/feature-card/feature-card.component.html b/desktop/angular/src/app/pages/dashboard/feature-card/feature-card.component.html new file mode 100644 index 00000000..0695555e --- /dev/null +++ b/desktop/angular/src/app/pages/dashboard/feature-card/feature-card.component.html @@ -0,0 +1,61 @@ +
+ + + + + + + + + + + +
+ + + + {{ feature?.Name }} + + +
+
+ +
+ + + + BETA +
+
+
+
+ + {{ (disabled ? 'Available in ' : '') + 'Portmaster ' + feature?.InPackage?.Name}} + {{ comingSoon ? ' - coming soon' : '' }} + {{ feature?.Comment }} + +
+ +
+ +
+
+ + Active + +
+ +
+ {{ feature?.InPackage?.Name }} + +
+
diff --git a/desktop/angular/src/app/pages/dashboard/feature-card/feature-card.component.scss b/desktop/angular/src/app/pages/dashboard/feature-card/feature-card.component.scss new file mode 100644 index 00000000..88f5fc61 --- /dev/null +++ b/desktop/angular/src/app/pages/dashboard/feature-card/feature-card.component.scss @@ -0,0 +1,60 @@ +.feature-card { + @apply flex flex-col p-4 bg-gray-300 rounded shadow w-full relative gap-2 overflow-hidden; + + .disabled-bg { + @apply absolute top-0 left-0 text-gray-500 opacity-50; + } + + &.disabled { + @apply opacity-80 shadow-inner; + } + + &.clickable { + @apply cursor-pointer; + &:hover { + @apply bg-gray-400; + } + } + + header { + @apply flex flex-row items-center justify-start gap-2 w-full; + + img { + @apply w-5 h-5; + filter: invert(1); + } + + &>span { + @apply text-base font-light; + } + } +} + +.ribbon { + width: 90px; + height: 100%; + overflow: hidden; + position: absolute; + top: 0px; + right: 0px; + z-index: 100; +} + +.ribbon__content { + left: -7px; + top: 25px; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); + position: absolute; + display: block; + width: 125px; + padding: 2px 0; + text-shadow: 0 1px 0px rgba(0, 0, 0, .2); + text-transform: uppercase; + text-align: center; + border: 2px dotted #fff; + outline-color: #fff; + outline-width: 1px; + outline-style: solid; +} diff --git a/desktop/angular/src/app/pages/dashboard/feature-card/feature-card.component.ts b/desktop/angular/src/app/pages/dashboard/feature-card/feature-card.component.ts new file mode 100644 index 00000000..8355a3ba --- /dev/null +++ b/desktop/angular/src/app/pages/dashboard/feature-card/feature-card.component.ts @@ -0,0 +1,128 @@ +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, SimpleChanges, inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { BoolSetting, ConfigService, Feature } from '@safing/portmaster-api'; +import { Subscription } from 'rxjs'; +import { INTEGRATION_SERVICE } from 'src/app/integration'; + +@Component({ + selector: 'app-feature-card', + templateUrl: './feature-card.component.html', + styleUrls: ['./feature-card.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FeatureCardComponent implements OnChanges, OnDestroy { + private readonly cdr = inject(ChangeDetectorRef); + private readonly configService = inject(ConfigService); + private readonly router = inject(Router); + private readonly integration = inject(INTEGRATION_SERVICE); + + private configValueSubscription = Subscription.EMPTY; + + @Input() + set disabled(v: any) { + this._disabled = coerceBooleanProperty(v) + } + get disabled() { return this._disabled } + _disabled = false; + + get comingSoon() { return this.feature?.ComingSoon || false } + + @Input() + feature?: Feature; + + planColor: string | null = null; + + configValue: boolean | undefined = undefined; + + ngOnChanges(changes: SimpleChanges): void { + if ('feature' in changes) { + this.configValueSubscription.unsubscribe(); + this.configValueSubscription = Subscription.EMPTY; + + if (!!this.feature?.ConfigKey) { + this.configValueSubscription = + this.configService.watch(this.feature!.ConfigKey) + .subscribe(value => { + this.configValue = value; + this.cdr.markForCheck(); + }); + } + + if (this.feature?.InPackage?.HexColor) { + this.planColor = getContrastFontColor(this.feature.InPackage.HexColor); + // console.log(this.feature.InPackage.HexColor, this.planColor) + this.cdr.markForCheck(); + } + } + } + + ngOnDestroy() { + this.configValueSubscription.unsubscribe(); + } + + updateSettingsValue(newValue: boolean) { + this.configService.save(this.feature!.ConfigKey, newValue) + .subscribe() + } + + navigateToConfigScope() { + if (this.disabled) { + this.integration.openExternal("https://safing.io/pricing?source=Portmaster") + return; + } + + let key: string | undefined; + if (this.feature?.ConfigScope) { + key = 'config:' + this.feature?.ConfigScope; + } else { + key = this.feature?.ConfigKey; + } + + if (!key) { + return + } + + + this.router.navigate(['/settings'], { + queryParams: { + setting: key, + } + }) + } +} + +function parseColor(input: string): number[] { + if (input.substr(0, 1) === '#') { + const collen = (input.length - 1) / 3; + const fact = [17, 1, 0.062272][collen - 1]; + return [ + Math.round(parseInt(input.substr(1, collen), 16) * fact), + Math.round(parseInt(input.substr(1 + collen, collen), 16) * fact), + Math.round(parseInt(input.substr(1 + 2 * collen, collen), 16) * fact), + ]; + } + + return input + .split('(')[1] + .split(')')[0] + .split(',') + .map((x) => +x); +} + +function getContrastFontColor(bgColor: string): string { + // if (red*0.299 + green*0.587 + blue*0.114) > 186 use #000000 else use #ffffff + // based on https://stackoverflow.com/a/3943023 + + let col = bgColor; + if (bgColor.startsWith('#') && bgColor.length > 7) { + col = bgColor.slice(0, 7); + } + const [r, g, b] = parseColor(col); + + if (r * 0.299 + g * 0.587 + b * 0.114 > 186) { + return '#000000'; + } + + return '#ffffff'; +} diff --git a/desktop/angular/src/app/pages/monitor/index.ts b/desktop/angular/src/app/pages/monitor/index.ts new file mode 100644 index 00000000..c21908c8 --- /dev/null +++ b/desktop/angular/src/app/pages/monitor/index.ts @@ -0,0 +1 @@ +export { MonitorPageComponent } from './monitor'; diff --git a/desktop/angular/src/app/pages/monitor/monitor.html b/desktop/angular/src/app/pages/monitor/monitor.html new file mode 100644 index 00000000..1d6fb177 --- /dev/null +++ b/desktop/angular/src/app/pages/monitor/monitor.html @@ -0,0 +1,46 @@ +
+ + +
+
+ +
+

+ Network Activity + +

+ + + + + + Network history data available as of {{ data.first | date }}. ({{ data.count }} connections) + + Clear + + + + + No network history data available. + + Enable + + + Available in Portmaster Plus + + + + + + + Use the search bar and drop downs to search and filter the last 10 minutes of network traffic. + Optionally, search all network history data if enabled. + +
+ + + +
diff --git a/desktop/angular/src/app/pages/monitor/monitor.scss b/desktop/angular/src/app/pages/monitor/monitor.scss new file mode 100644 index 00000000..12345b56 --- /dev/null +++ b/desktop/angular/src/app/pages/monitor/monitor.scss @@ -0,0 +1,49 @@ +:host { + overflow: hidden; + flex-direction: row; + flex-grow: 1; + width: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + padding-left: 1.7rem; + padding-right: 0.8rem; + + .header, + .content { + padding: 0; + margin: 0; + } + + .header { + padding-top: 0.9rem; + + .breadcrumbs { + font-size: 0.715rem; + font-weight: 500; + color: #cacaca; + user-select: none; + display: flex; + + span:first-child { + opacity: .55; + font-weight: 400; + margin-right: 4px; + + &:hover { + opacity: 1; + } + } + + svg.arrow { + width: 1rem; + padding: 0; + margin: 0; + + .inner { + stroke: white; + } + } + } + } +} diff --git a/desktop/angular/src/app/pages/monitor/monitor.ts b/desktop/angular/src/app/pages/monitor/monitor.ts new file mode 100644 index 00000000..d6ce3374 --- /dev/null +++ b/desktop/angular/src/app/pages/monitor/monitor.ts @@ -0,0 +1,77 @@ +import { Component, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { BoolSetting, ConfigService, Database, FeatureID, Netquery, SPNService } from '@safing/portmaster-api'; +import { Subject, interval, map, merge, repeat } from 'rxjs'; +import { SessionDataService } from 'src/app/services'; +import { ActionIndicatorService } from 'src/app/shared/action-indicator'; +import { fadeInAnimation, moveInOutListAnimation } from 'src/app/shared/animations'; + +@Component({ + templateUrl: './monitor.html', + styleUrls: ['../page.scss', './monitor.scss'], + providers: [], + animations: [fadeInAnimation, moveInOutListAnimation], +}) +export class MonitorPageComponent { + session = inject(SessionDataService); + netquery = inject(Netquery); + reload = new Subject(); + + configService = inject(ConfigService); + uai = inject(ActionIndicatorService); + + historyEnabled = inject(ConfigService) + .watch('history/enable'); + + canUseHistory = inject(SPNService).profile$ + .pipe( + map(profile => { + return profile?.current_plan?.feature_ids?.includes(FeatureID.History) || false; + }) + ); + + history = inject(Netquery) + .query({ + select: [ + { + $min: { + field: "started", + as: "first_connection", + }, + }, + { + $count: { + field: "*", + as: "totalCount" + } + } + ], + databases: [Database.History] + }, 'monitor-get-first-history-connection') + .pipe( + repeat({ delay: () => merge(interval(10000), this.reload) }), + map(result => { + if (!result.length || result[0].totalCount === 0) { + return null + } + + return { + first: new Date(result[0].first_connection), + count: result[0].totalCount, + } + }), + takeUntilDestroyed() + ); + + enableHistory() { + this.configService.save('history/enable', true) + .subscribe(); + } + + clearHistoryData() { + this.netquery.cleanProfileHistory([]) + .subscribe(() => { + this.reload.next(); + }) + } +} diff --git a/desktop/angular/src/app/pages/page.scss b/desktop/angular/src/app/pages/page.scss new file mode 100644 index 00000000..1977b027 --- /dev/null +++ b/desktop/angular/src/app/pages/page.scss @@ -0,0 +1,6 @@ +:host { + display : flex; + flex-direction: column; + width : 100%; + height : 100%; +} diff --git a/desktop/angular/src/app/pages/settings/settings.html b/desktop/angular/src/app/pages/settings/settings.html new file mode 100644 index 00000000..f85c752b --- /dev/null +++ b/desktop/angular/src/app/pages/settings/settings.html @@ -0,0 +1,26 @@ + + +
+

+ Global Settings + +

+
+ + + diff --git a/desktop/angular/src/app/pages/settings/settings.scss b/desktop/angular/src/app/pages/settings/settings.scss new file mode 100644 index 00000000..bc178ab8 --- /dev/null +++ b/desktop/angular/src/app/pages/settings/settings.scss @@ -0,0 +1,83 @@ +.header-title { + display: flex; + width: 100%; + padding-left: 3rem; + padding-right: 1.25rem; + margin-bottom: 0.5rem; + align-items: center; + height: 3rem; + flex-shrink: 0; + + h1{ + flex-grow: unset; + } + + fa-icon[icon*="question-circle"]{ + margin-left: 0.35rem; + } +} + +.card-title.meta { + div { + display: inline-block; + @apply mr-2; + } +} + +.columns { + width : 100%; + display : flex; + flex-direction: row; +} + +.meta { + + span:first-of-type { + @apply text-secondary; + @apply mr-1; + } +} + +.col { + flex-grow: 1; +} + +.unstable { + @apply text-xs; + @apply uppercase; + color: theme('colors.info.yellow'); +} + +sfng-accordion-group { + @apply pl-12; + @apply pr-4; // align with the scroll bar on the right side + @apply my-4; +} + +div.tableFixHead { + @apply mt-4; + @apply rounded-t; + + &:not(.empty) { + @apply rounded; + } + + max-height: 16rem; +} + +.cdk-row.unused { + opacity: 0.4; +} + +.card-actions { + display : flex; + align-items: center; + + * { + @apply ml-2; + } + + app-menu-trigger { + display: inline-block; + } +} diff --git a/desktop/angular/src/app/pages/settings/settings.ts b/desktop/angular/src/app/pages/settings/settings.ts new file mode 100644 index 00000000..3f36f9bd --- /dev/null +++ b/desktop/angular/src/app/pages/settings/settings.ts @@ -0,0 +1,133 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { ConfigService, Setting } from '@safing/portmaster-api'; +import { Subscription } from 'rxjs'; +import { StatusService, VersionStatus } from 'src/app/services'; +import { ActionIndicatorService } from 'src/app/shared/action-indicator'; +import { fadeInAnimation } from 'src/app/shared/animations'; +import { SaveSettingEvent } from 'src/app/shared/config/generic-setting/generic-setting'; + +@Component({ + templateUrl: './settings.html', + styleUrls: [ + '../page.scss', + './settings.scss' + ], + animations: [fadeInAnimation] +}) +export class SettingsComponent implements OnInit, OnDestroy { + /** @private The current search term for the settings. */ + searchTerm: string = ''; + + /** @private All settings currently displayed. */ + settings: Setting[] = []; + + /** @private The available and selected resource versions. */ + versions: VersionStatus | null = null; + + /** + * @private + * The key of the setting to highligh, if any ... + */ + highlightSettingKey: string | null = null; + + /** Subscription to watch all available settings. */ + private subscription = Subscription.EMPTY; + + constructor( + public configService: ConfigService, + public statusService: StatusService, + private actionIndicator: ActionIndicatorService, + private route: ActivatedRoute, + ) { } + + ngOnInit(): void { + this.subscription = new Subscription(); + + this.loadSettings(); + + // Request the current resource versions once. We add + // it to the subscription to prevent a memory leak in + // case the user leaves the page before the versions + // have been loaded. + const versionSub = this.statusService.getVersions() + .subscribe(version => this.versions = version); + + this.subscription.add(versionSub); + + const querySub = this.route.queryParamMap + .subscribe( + params => { + this.highlightSettingKey = params.get('setting'); + } + ) + this.subscription.add(querySub); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + /** + * Loads all settings from the portmaster. + */ + private loadSettings() { + const configSub = this.configService.query('') + .subscribe(settings => this.settings = settings); + this.subscription.add(configSub); + } + + /** + * @private + * SaveSettingEvent is emitted by the settings-view + * component when a value has been changed and should be saved. + * + * @param event The save-settings event + */ + saveSetting(event: SaveSettingEvent) { + let idx = this.settings.findIndex(setting => setting.Key === event.key); + if (idx < 0) { + return; + } + + const setting = { + ...this.settings[idx], + } + + if (event.isDefault) { + delete (setting['Value']); + } else { + setting.Value = event.value; + } + + this.configService.save(setting) + .subscribe({ + next: () => { + if (!!event.accepted) { + event.accepted(); + } + + this.settings[idx] = setting; + + // copy the settings into a new array so we trigger + // an input update due to changed array identity. + this.settings = [...this.settings]; + + // for the release level setting we need to + // to a page-reload since portmaster will now + // return more settings. + if (setting.Key === 'core/releaseLevel') { + this.loadSettings(); + } + }, + error: err => { + if (!!event.rejected) { + event.rejected(err); + } + + this.actionIndicator.error('Failed to save setting', err); + console.error(err); + } + }) + } +} diff --git a/desktop/angular/src/app/pages/spn/country-details/country-details.html b/desktop/angular/src/app/pages/spn/country-details/country-details.html new file mode 100644 index 00000000..f4e43963 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/country-details/country-details.html @@ -0,0 +1,154 @@ +

+ + {{ countryName }} + + + + + +

+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Total Nodes + {{ totalAliveCount }}
+ + + by Safing + + {{ safingNodeCount }}
+ + + by Community + + {{ communityNodeCount }}
+ Exit Nodes + {{ exitNodeCount }}
+ + + by Safing + + {{ safingExitNodeCount }}
+ + + by Community + + {{ communityExitNodeCount }}
+ Nodes In Use + {{ activeNodeCount }}
+ + + by Safing + + {{ activeSafingNodeCount }}
+ + + by Community + + {{ activeCommunityNodeCount }}
+
+
+ + + + +
+ The following Apps have connections that are routed through the + SPN and use an + exit node in {{ countryName }} ({{ countryCode }}): + + + + + + + + +
+ + {{ app.profile.Name }} + + {{ app.count }} connections + +
+
+ + + + + + +
+ +
+ + + + +
+
+
+
+
+ +
diff --git a/desktop/angular/src/app/pages/spn/country-details/country-details.ts b/desktop/angular/src/app/pages/spn/country-details/country-details.ts new file mode 100644 index 00000000..3fa34f1c --- /dev/null +++ b/desktop/angular/src/app/pages/spn/country-details/country-details.ts @@ -0,0 +1,217 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnChanges, OnDestroy, OnInit, Optional, Output, SimpleChanges, TrackByFunction } from "@angular/core"; +import { AppProfile, AppProfileService, Netquery } from '@safing/portmaster-api'; +import { SFNG_DIALOG_REF, SfngDialogRef, SfngDialogService } from "@safing/ui"; +import { Subscription, forkJoin, of, switchMap } from 'rxjs'; +import { repeat } from 'rxjs/operators'; +import { MapPin, MapService } from './../map.service'; +import { PinDetailsComponent } from './../pin-details/pin-details'; + +@Component({ + templateUrl: './country-details.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styles: [ + `:host{ + display: block; + min-width: 630px; + height: 400px; + overflow: hidden; + }` + ] +}) +export class CountryDetailsComponent implements OnInit, OnChanges, OnDestroy { + /** Subscription to poll map pins and profiles. */ + private subscription = Subscription.EMPTY; + + /** The two letter ISO country code */ + @Input() + countryCode: string = ''; + + /** The name of the country */ + @Input() + countryName: string = ''; + + /** Emits the ID of the pin that is hovered in the list. null if no pin is hovered */ + @Output() + pinHover = new EventEmitter(); + + /** @private - The list of pins available in this country */ + pins: MapPin[] = []; + + /** @private - A list of app profiles that use this country as an exit node */ + profiles: { profile: AppProfile, count: number }[] = []; + + /** @private - A {@link TrackByFunction} for all profiles that use this country for exit */ + trackProfile: TrackByFunction = (_: number, profile: this['profiles'][0]) => `${profile.profile.Source}/${profile.profile.ID}`; + + /** The number of alive nodes in this country */ + totalAliveCount = 0; + + /** The number of exit nodes in this country */ + exitNodeCount = 0; + + /** The number of active (used) nodes in this country */ + activeNodeCount = 0; + + /** The number of active (used) nodes operated by safing */ + activeSafingNodeCount = 0; + + /** The number of active (used) nodes operated by the community */ + activeCommunityNodeCount = 0; + + /** The number of nodes operated by safing */ + safingNodeCount = 0; + + /** The number of exit nodes operated by safing */ + safingExitNodeCount = 0; + + /** The number of nodes operated by a community member */ + communityNodeCount = 0; + + /** The number of exit ndoes operated by the community */ + communityExitNodeCount = 0; + + /** holds the text format of a netquery search to show all connections that exit in this country */ + filterConnectionsByCountryNodes = ''; + + constructor( + private mapService: MapService, + private netquery: Netquery, + private appService: AppProfileService, + private cdr: ChangeDetectorRef, + private dialog: SfngDialogService, + @Inject(SFNG_DIALOG_REF) @Optional() public dialogRef?: SfngDialogRef, + ) { } + + openPinDetails(id: string) { + this.dialog.create(PinDetailsComponent, { + data: id, + backdrop: false, + autoclose: true, + dragable: true, + }) + } + + ngOnInit() { + // if we got opened as a dialog we get the code and name of the country + // from the dialogRef.data field. + if (!!this.dialogRef) { + this.countryCode = this.dialogRef.data.code; + this.countryName = this.dialogRef.data.name; + } + + this.subscription.unsubscribe(); + + this.subscription = + this.mapService + .pins$ + .pipe( + switchMap(pins => { + // get a list of pins in that country + const countryPins = pins.filter(pin => pin.entity.Country === this.countryCode); + + // prepare a netquery query that loads the IDs of all profiles that use one of the countries + // pins as an exit node. Then, map those IDs to the actual app profile object + const profiles = this.netquery + .query({ + select: [ + 'profile', + { $count: { field: '*', as: 'totalCount' } } + ], + groupBy: ['profile'], + query: { + 'exit_node': { + $in: countryPins.map(pin => pin.pin.ID), + } + } + }, 'get-connections-per-profile-in-country') + .pipe( + switchMap(queryResult => { + if (queryResult.length === 0) { + return of([]); + } + + return forkJoin( + queryResult.map(row => forkJoin({ + profile: this.appService.getAppProfile(row.profile!), + count: of(row.totalCount), + }) + ) + ) + }), + ); + + return forkJoin({ + pins: of(countryPins), + profiles: profiles, + }) + } + ), + repeat({ + delay: 5000 + }), + ) + .subscribe(result => { + this.pins = result.pins; + this.profiles = result.profiles + + this.activeNodeCount = 0; + this.activeCommunityNodeCount = 0; + this.activeSafingNodeCount = 0; + this.exitNodeCount = 0; + this.safingNodeCount = 0; + this.communityNodeCount = 0; + this.safingExitNodeCount = 0; + this.communityExitNodeCount = 0; + + this.pins.forEach(pin => { + if (pin.isOffline) { + return + } + this.totalAliveCount++; + + if (pin.pin.VerifiedOwner === 'Safing') { + this.safingNodeCount++; + + if (pin.isExit) { + this.exitNodeCount++; + this.safingExitNodeCount++; + } + if (pin.isActive) { + this.activeSafingNodeCount++; + this.activeNodeCount++; + } + + } else { + this.communityNodeCount++; + + if (pin.isExit) { + this.exitNodeCount++; + this.communityExitNodeCount++; + } + if (pin.isActive) { + this.activeCommunityNodeCount++; + this.activeNodeCount++; + } + } + }) + + // create a netquery text-query in the format of "exit_node: exit_node: ..." + this.filterConnectionsByCountryNodes = this.pins.map(pin => `exit_node:${pin.pin.ID}`).join(" ") + + this.cdr.markForCheck(); + }) + } + + ngOnChanges(changes: SimpleChanges): void { + // if we are rendered as a regular component (not as a dialog) we need to + // handle updates to our @Inputs(). + // just let ngOnInit() do it's thing if the countryCode changed. + if (!!changes['countryCode']) { + this.ngOnInit(); + } + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } +} diff --git a/desktop/angular/src/app/pages/spn/country-details/index.ts b/desktop/angular/src/app/pages/spn/country-details/index.ts new file mode 100644 index 00000000..3ff2c685 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/country-details/index.ts @@ -0,0 +1 @@ +export * from './country-details'; diff --git a/desktop/angular/src/app/pages/spn/country-overlay/country-overlay.html b/desktop/angular/src/app/pages/spn/country-overlay/country-overlay.html new file mode 100644 index 00000000..6ea2166f --- /dev/null +++ b/desktop/angular/src/app/pages/spn/country-overlay/country-overlay.html @@ -0,0 +1,25 @@ + + + + + {{ countryName }} + + + + + + Safing Nodes: + {{ safingNodes.length }} + + + + + Community Nodes: + {{ communityNodes.length }} + + + + + Click country for details + + diff --git a/desktop/angular/src/app/pages/spn/country-overlay/country-overlay.scss b/desktop/angular/src/app/pages/spn/country-overlay/country-overlay.scss new file mode 100644 index 00000000..be98030c --- /dev/null +++ b/desktop/angular/src/app/pages/spn/country-overlay/country-overlay.scss @@ -0,0 +1,40 @@ +:host { + @apply flex flex-row items-center justify-center; +} + +.country-content-wrapper { + @apply flex flex-col gap-2 items-center justify-center bg-gray-200 border bg-opacity-50 border-gray-600 border-opacity-25; +} + +.country-name { + @apply text-sm flex flex-row gap-1 items-center justify-center bg-gray-100 bg-opacity-50 py-2 w-full; +} + +.country-stats { + @apply flex flex-col gap-2 items-start py-2 px-4; + + &>span { + @apply flex flex-row gap-1 items-center; + @apply text-xs font-light; + } + + .count { + @apply text-sm font-normal; + } +} + +.country-stats--safing { + svg polygon { + fill: #0376bb; + stroke: #0376bb; + transform: scale(1.15) + } +} + +.country-stats--community { + svg circle { + fill: #239215; + stroke: #239215; + transform: scale(1.15) + } +} diff --git a/desktop/angular/src/app/pages/spn/country-overlay/country-overlay.ts b/desktop/angular/src/app/pages/spn/country-overlay/country-overlay.ts new file mode 100644 index 00000000..2af05a26 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/country-overlay/country-overlay.ts @@ -0,0 +1,75 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; +import { BehaviorSubject, combineLatest, map } from 'rxjs'; +import { takeWhile } from 'rxjs/operators'; +import { MapPin, MapService } from './../map.service'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'spn-map-country-overlay', + templateUrl: './country-overlay.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: [ + './country-overlay.scss' + ] +}) +export class CountryOverlayComponent implements OnInit, OnChanges, OnDestroy { + /** The two-letter ISO code of the country */ + @Input() + countryCode!: string; + + /** The (english) name of the country */ + @Input() + countryName!: string; + + /** all nodes in this country operated by Safing */ + safingNodes: MapPin[] = []; + + /** all nodes in this country operated by a community member */ + communityNodes: MapPin[] = []; + + /** used to trigger a reload onChanges */ + private reload$ = new BehaviorSubject(undefined); + + constructor( + private mapService: MapService, + private cdr: ChangeDetectorRef, + ) { } + + ngOnChanges(changes: SimpleChanges): void { + this.reload$.next(); + } + + ngOnInit(): void { + combineLatest([ + this.mapService.pins$, + this.reload$ + ]) + .pipe( + takeWhile(() => !this.reload$.closed), + map(([pins]) => pins.filter(pin => pin.entity.Country === this.countryCode)), + ) + .subscribe(pinsInCountry => { + this.safingNodes = []; + this.communityNodes = []; + + pinsInCountry.forEach(pin => { + if (pin.isOffline && !pin.isActive) { + return + } + + if (pin.pin.VerifiedOwner === 'Safing') { + this.safingNodes.push(pin) + } else { + this.communityNodes.push(pin) + } + }) + + this.cdr.markForCheck(); + }) + } + + ngOnDestroy(): void { + this.reload$.complete(); + } +} + diff --git a/desktop/angular/src/app/pages/spn/country-overlay/index.ts b/desktop/angular/src/app/pages/spn/country-overlay/index.ts new file mode 100644 index 00000000..2e61e978 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/country-overlay/index.ts @@ -0,0 +1 @@ +export * from './country-overlay'; diff --git a/desktop/angular/src/app/pages/spn/index.ts b/desktop/angular/src/app/pages/spn/index.ts new file mode 100644 index 00000000..cc24eeea --- /dev/null +++ b/desktop/angular/src/app/pages/spn/index.ts @@ -0,0 +1 @@ +export * from './spn-page'; diff --git a/desktop/angular/src/app/pages/spn/map-legend/index.ts b/desktop/angular/src/app/pages/spn/map-legend/index.ts new file mode 100644 index 00000000..111ec8c0 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/map-legend/index.ts @@ -0,0 +1 @@ +export * from './map-legend'; diff --git a/desktop/angular/src/app/pages/spn/map-legend/map-legend.html b/desktop/angular/src/app/pages/spn/map-legend/map-legend.html new file mode 100644 index 00000000..07cf2a9b --- /dev/null +++ b/desktop/angular/src/app/pages/spn/map-legend/map-legend.html @@ -0,0 +1,54 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Safing Nodes + {{ safingNodeCount }}
+ + + used as Transit + + {{ safingActiveCount }}
+ + + used as Exit + + {{ safingExitCount }}
+ + Community Nodes + {{ communityNodeCount }}
+ + + used as Transit + + {{ communityActiveCount }}
+ + + used as Exit + + {{ communityExitCount }}
+
diff --git a/desktop/angular/src/app/pages/spn/map-legend/map-legend.ts b/desktop/angular/src/app/pages/spn/map-legend/map-legend.ts new file mode 100644 index 00000000..e561111e --- /dev/null +++ b/desktop/angular/src/app/pages/spn/map-legend/map-legend.ts @@ -0,0 +1,69 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; +import { Subscription } from 'rxjs'; +import { MapService } from './../map.service'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'spn-map-legend', + templateUrl: './map-legend.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SpnMapLegendComponent implements OnInit, OnDestroy { + private subscription = Subscription.EMPTY; + + safingNodeCount = 0; + safingExitCount = 0; + safingActiveCount = 0; + + communityNodeCount = 0; + communityExitCount = 0; + communityActiveCount = 0; + + constructor( + private mapService: MapService, + private cdr: ChangeDetectorRef, + ) { } + + ngOnInit() { + this.subscription = this.mapService + .pins$ + .subscribe(pins => { + this.safingActiveCount = 0; + this.safingExitCount = 0; + this.safingNodeCount = 0; + this.communityActiveCount = 0; + this.communityExitCount = 0; + this.communityNodeCount = 0; + + pins.forEach(pin => { + if (pin.pin.VerifiedOwner === 'Safing') { + if (pin.isActive) { + this.safingActiveCount++; + } + + if (pin.isExit) { + this.safingExitCount++ + } + + this.safingNodeCount++ + } else { + if (pin.isActive) { + this.communityActiveCount++; + } + + if (pin.isExit) { + this.communityExitCount++; + } + + this.communityNodeCount++; + } + }) + + this.cdr.markForCheck(); + }) + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } +} diff --git a/desktop/angular/src/app/pages/spn/map-renderer/index.ts b/desktop/angular/src/app/pages/spn/map-renderer/index.ts new file mode 100644 index 00000000..60ff8a16 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/map-renderer/index.ts @@ -0,0 +1 @@ +export * from './map-renderer'; diff --git a/desktop/angular/src/app/pages/spn/map-renderer/map-renderer.ts b/desktop/angular/src/app/pages/spn/map-renderer/map-renderer.ts new file mode 100644 index 00000000..bea5ee57 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/map-renderer/map-renderer.ts @@ -0,0 +1,383 @@ +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, ElementRef, Inject, InjectionToken, Input, OnDestroy, OnInit, Optional, inject } from '@angular/core'; +import { GeoPath, GeoPermissibleObjects, GeoProjection, Selection, ZoomTransform, geoMercator, geoPath, json, pointer, select, zoom, zoomIdentity } from 'd3'; +import { feature } from 'topojson-client'; + + +export type MapRoot = Selection; +export type WorldGroup = Selection + +export interface CountryEvent { + event?: MouseEvent; + countryCode: string; + countryName: string; +} + +export interface MapRef { + onMapReady(cb: () => any): void; + onZoomPan(cb: () => any): void; + onCountryHover(cb: (_: CountryEvent | null) => void): void; + onCountryClick(cb: (_: CountryEvent) => void): void; + select(selection: string): Selection | null; + + countryNames: { [key: string]: string }; + root: MapRoot; + projection: GeoProjection; + zoomScale: number; + worldGroup: WorldGroup; +} + +export interface MapHandler { + registerMap(ref: MapRef): void; + unregisterMap(ref: MapRef): void; +} + +export const MAP_HANDLER = new InjectionToken('MAP_HANDLER'); + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'spn-map-renderer', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + styleUrls: [ + './map-style.scss' + ], +}) +export class MapRendererComponent implements OnInit, AfterViewInit, OnDestroy { + static readonly Rotate = 0; // so [-0, 0] is the initial center of the projection + static readonly Maxlat = 83; // clip northern and southern pols (infinite in mercator) + static readonly MarkerSize = 4; + static readonly LineAnimationDuration = 200; + + private readonly destroyRef = inject(DestroyRef); + private destroyed = false; + + countryNames: { + [countryCode: string]: string + } = {} + + // SVG group elements + private svg: MapRoot | null = null; + worldGroup!: WorldGroup; + + // Projection and line rendering functions + projection!: GeoProjection; + zoomScale: number = 1 + + private pathFunc!: GeoPath; + + get root() { + return this.svg! + } + + @Input() + mapId: string = 'map' + + constructor( + private mapRoot: ElementRef, + private cdr: ChangeDetectorRef, + @Inject(MAP_HANDLER) @Optional() private overlays: MapHandler[], + ) { } + + ngOnInit(): void { + this.overlays?.forEach(ov => { + ov.registerMap(this) + }) + + this.cdr.detach() + } + + select(selector: string) { + if (!this.svg) { + return null + } + + return this.svg.select(selector); + } + + private _readyCb: (() => void)[] = []; + onMapReady(cb: () => void) { + this._readyCb.push(cb); + } + + private _zoomCb: (() => void)[] = []; + onZoomPan(cb: () => void) { + this._zoomCb.push(cb) + } + + private _countryHoverCb: ((e: CountryEvent | null) => void)[] = []; + onCountryHover(cb: (e: CountryEvent | null) => void) { + this._countryHoverCb.push(cb); + } + + private _countryClickCb: ((e: CountryEvent) => void)[] = []; + onCountryClick(cb: (e: CountryEvent) => void) { + this._countryClickCb.push(cb) + } + + async ngAfterViewInit() { + await this.renderMap() + + const observer = new ResizeObserver(() => { + this.renderMap() + }) + + this.destroyRef.onDestroy(() => { + observer.unobserve(this.mapRoot.nativeElement) + observer.disconnect() + }) + + observer.observe(this.mapRoot.nativeElement); + } + + async renderMap() { + if (this.destroyed) { + return; + } + + if (!!this.svg) { + this.svg.remove() + } + + const map = select(this.mapRoot.nativeElement); + + // setup the basic SVG elements + this.svg = map + .append('svg') + .attr('id', this.mapId) + .attr("xmlns", "http://www.w3.org/2000/svg") + .attr('width', '100%') + .attr('preserveAspectRation', 'none') + .attr('height', '100%') + + this.worldGroup = this.svg.append('g').attr('id', 'world-group') + + // load the world-map data and start rendering + const world = await json('/assets/world-50m.json'); + + // actually render the countries + const countries = (feature(world, world.objects.countries) as any); + + this.setupProjection(); + await this.setupZoom(countries); + + // we need to await the initial world render here because otherwise + // the initial renderPins() will not be able to update the country attributes + // and cause a delay before the state of the country (has-nodes, is-blocked, ...) + // is visible. + this.renderWorld(countries); + + this._readyCb.forEach(cb => cb()); + } + + ngOnDestroy() { + this.destroyed = true; + + this.overlays?.forEach(ov => ov.unregisterMap(this)); + + this._countryClickCb = []; + this._countryHoverCb = []; + this._readyCb = []; + this._zoomCb = []; + + if (!this.svg) { + return; + } + + this.svg.remove(); + this.svg = null; + } + + private renderWorld(countries: any) { + // actually render the countries + const data = countries.features; + const self = this; + + data.forEach((country: any) => { + this.countryNames[country.properties.iso_a2] = country.properties.name + }) + + this.worldGroup.selectAll() + .data(data) + .enter() + .append('path') + .attr('countryCode', (d: any) => d.properties.iso_a2) + .attr('name', (d: any) => d.properties.name) + .attr('d', this.pathFunc) + .on('mouseenter', function (event: MouseEvent) { + const country = select(this).datum() as any; + const countryEvent: CountryEvent = { + event: event, + countryCode: country.properties.iso_a2, + countryName: country.properties.name, + } + + self._countryHoverCb.forEach(cb => cb(countryEvent)) + }) + .on('mouseout', function (event: MouseEvent) { + self._countryHoverCb.forEach(cb => cb(null)) + }) + .on('click', function (event: MouseEvent) { + const country = select(this).datum() as any; + const countryEvent: CountryEvent = { + event: event, + countryCode: country.properties.iso_a2, + countryName: country.properties.name, + } + + const loc = self.projection.invert!([event.clientX, event.clientY]) + + console.log(loc) + + self._countryClickCb.forEach(cb => cb(countryEvent)) + }) + } + + private setupProjection() { + const size = this.mapRoot.nativeElement.getBoundingClientRect(); + + this.projection = geoMercator() + .rotate([MapRendererComponent.Rotate, 0]) + .scale(1) + .translate([size.width / 2, size.height / 2]); + + + // path is used to update the SVG path to match our mercator projection + this.pathFunc = geoPath().projection(this.projection); + } + + private async setupZoom(countries: any) { + if (!this.svg) { + return + } + + // create a copy of countries + countries = { + ...countries, + features: [...countries.features] + } + + // remove Antarctica from the feature set so projection.fitSize ignores it + // and better aligns the rest of the world :) + const aqIdx = countries.features.findIndex((p: GeoJSON.Feature) => p.properties?.iso_a2 === "AQ"); + if (aqIdx >= 0) { + countries.features.splice(aqIdx, 1) + } + + const size = this.mapRoot.nativeElement.getBoundingClientRect(); + + this.projection.fitSize([size.width, size.height], countries) + + //this.projection.fitWidth(size.width, countries) + //this.projection.fitHeight(size.height, countries) + + // returns the top-left and the bottom-right of the current projection + const mercatorBounds = () => { + const yaw = this.projection.rotate()[0]; + const xymax = this.projection([-yaw + 180 - 1e-6, -MapRendererComponent.Maxlat])!; + const xymin = this.projection([-yaw - 180 + 1e-6, MapRendererComponent.Maxlat])!; + return [xymin, xymax]; + } + + const s = this.projection.scale() + const scaleExtent = [s, s * 10] + + const transform = zoomIdentity + .scale(this.projection.scale()) + .translate(this.projection.translate()[0], this.projection.translate()[1]); + + // whenever the users zooms we need to update our groups + // individually to apply the zoom effect. + let tlast = { + x: 0, + y: 0, + k: 0, + } + + const self = this; + + let z = zoom() + .scaleExtent(scaleExtent as [number, number]) + .on('zoom', (e) => { + const t: ZoomTransform = e.transform; + + if (t.k != tlast.k) { + let p = pointer(e) + let scrollToMouse = () => { }; + + if (!!p && !!p[0]) { + const tp = this.projection.translate(); + const coords = this.projection!.invert!(p) + scrollToMouse = () => { + const newPos = this.projection(coords!)!; + const yaw = this.projection.rotate()[0]; + this.projection.translate([tp[0], tp[1] + (p[1] - newPos[1])]) + this.projection.rotate([yaw + 360.0 * (p[0] - newPos[0]) / size.width * scaleExtent[0] / t.k, 0, 0]) + } + } + + this.projection.scale(t.k); + scrollToMouse(); + + } else { + let dy = t.y - tlast.y; + const dx = t.x - tlast.x; + const yaw = this.projection.rotate()[0] + const tp = this.projection.translate(); + + // use x translation to rotate based on current scale + this.projection.rotate([yaw + 360.0 * dx / size.width * scaleExtent[0] / t.k, 0, 0]) + // use y translation to translate projection clamped to bounds + let bounds = mercatorBounds(); + if (bounds[0][1] + dy > 0) { + dy = -bounds[0][1]; + } else if (bounds[1][1] + dy < size.height) { + dy = size.height - bounds[1][1]; + } + this.projection.translate([tp[0], tp[1] + dy]); + } + + tlast = { + x: t.x, + y: t.y, + k: t.k, + } + + // finally, re-render the SVG shapes according to the new projection + this.worldGroup.selectAll('path') + .attr('d', this.pathFunc) + + + this._zoomCb.forEach(cb => cb()); + }); + + this.svg.call(z) + this.svg.call(z.transform, transform); + } + + public getCoords(lat: number, lng: number) { + const loc = this.projection([lng, lat]); + if (!loc) { + return null; + } + + const rootElem = this.mapRoot.nativeElement.getBoundingClientRect(); + const x = rootElem.x + loc[0]; + const y = rootElem.y + loc[1]; + + return [x, y]; + } + + public coordsInView(lat: number, lng: number) { + const loc = this.projection([lng, lat]); + if (!loc) { + return false + } + + const rootElem = this.mapRoot.nativeElement.getBoundingClientRect(); + const x = rootElem.x + loc[0]; + const y = rootElem.y + loc[1]; + + return x >= rootElem.left && x <= rootElem.right && y >= rootElem.top && y <= rootElem.bottom; + } + +} diff --git a/desktop/angular/src/app/pages/spn/map-renderer/map-style.scss b/desktop/angular/src/app/pages/spn/map-renderer/map-style.scss new file mode 100644 index 00000000..0f319ba7 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/map-renderer/map-style.scss @@ -0,0 +1,167 @@ +::ng-deep { + .pin { + opacity: 0; + + &.in-view { + opacity: 1; + } + } +} + +::ng-deep #spn-map { + --map-bg: #111112; + --map-country-active: #424141; + --map-country-inactive: #2a2a2a; + --map-country-border-width: 2px; + --map-country-border-color: #1e1e1e; + --map-country-border-color-selected: #858585; + --map-country-blocked-primary: #858585; + --map-country-blocked-secondary: #402323; + + .overlay { + fill: none; + pointer-events: all; + } + + g { + + circle, + polygon { + fill: #626262; + stroke: #626262; + stroke-width: 1; + stroke-linejoin: round; + transition: all 200ms linear 0s; + } + + circle:hover, + polygon:hover { + fill: theme('colors.yellow.200'); + stroke: theme('colors.yellow.300'); + stroke-width: 2; + } + } + + g[in-use=true] { + circle { + fill: #239215; + stroke: #239215; + transform: scale(1.15) + } + + polygon { + fill: #0376bb; + stroke: #0376bb; + transform: scale(1.15) + } + } + + g[is-exit=true] { + + circle, + polygon { + transform: scale(1.3); + stroke-width: 2; + } + + polygon { + stroke: #039af4; + fill: #0376bb; + } + + circle { + stroke: #30ae20; + fill: #239215; + } + } + + g[is-home=true] circle { + stroke: white; + stroke-width: 4.5; + fill: black; + transform: scale(1); + } + + g[raise=true] { + + circle, + polygon { + fill: theme('colors.yellow.200'); + stroke: theme('colors.yellow.300'); + stroke-width: 2; + transform: scale(1.8); + } + } + + .marker { + cursor: pointer; + fill: #252525; + stroke: rgba(151, 151, 151, 0.8); + transition: all 250ms 0s cubic-bezier(0.175, 0.885, 0.32, 1.275); + } + + .marker-label { + fill: white; + } + + path.lane { + stroke: rgba(151, 151, 151, 0.2); + fill: transparent; + + &[in-use=true] { + stroke-width: 2; + stroke: #0376bb; + } + + &[is-live=true] { + stroke-width: 1; + stroke: theme('colors.red.300'); + + &[is-encrypted=true] { + stroke: theme('colors.green.200'); + } + + &:hover { + stroke-width: 3; + } + } + } + + #world-group { + path { + fill: var(--map-country-border-color); + stroke: var(--map-country-border-color); + stroke-width: var(--map-country-border-width); + stroke-linejoin: round; + } + + path[has-nodes=true] { + fill: var(--map-country-inactive); + } + + path[in-use=true] { + fill: var(--map-country-active); + } + + path:hover { + cursor: pointer; + fill: var(--map-country-active); + } + + path.selected { + stroke: var(--map-country-border-color-selected); + } + } +} + +:host-context(.disabled) { + @apply bg-white; + + #world-group { + path { + fill: #000000; + stroke: #111111; + stroke-width: .5px; + } + } +} diff --git a/desktop/angular/src/app/pages/spn/map.service.ts b/desktop/angular/src/app/pages/spn/map.service.ts new file mode 100644 index 00000000..da8041a9 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/map.service.ts @@ -0,0 +1,253 @@ +import { Injectable } from '@angular/core'; +import { AppProfile, GeoCoordinates, IntelEntity, Netquery, Pin, SPNService, UnknownLocation, getPinCoords } from '@safing/portmaster-api'; +import { BehaviorSubject, Observable, combineLatest, debounceTime, interval, of, startWith, switchMap } from 'rxjs'; +import { distinctUntilChanged, filter, map, share } from 'rxjs/operators'; +import { SPNStatus } from './../../../../projects/safing/portmaster-api/src/lib/spn.types'; + +export interface MapPin { + pin: Pin; + // location is set to the geo-coordinates that should be used + // for that pin. + location: GeoCoordinates; + // entity is set to the intel entity that should be used for + // this pin. + entity: IntelEntity; + + // whether the pin is regarded as offline / not available. + isOffline: boolean; + + // whether or not the pin is currently used as an exit node + isExit: boolean; + + // whether or not the pin is used as a transit node + isTransit: boolean; + + // whether or not the pin is currently active. + isActive: boolean; + + // whether or not the pin is used as the entry-node. + isHome: boolean; + + // whether the pin has any known issues + hasIssues: boolean; + + // FIXME: remove me + collapsed?: boolean; +} + +@Injectable({ providedIn: 'root' }) +export class MapService { + /** + * activeSince$ emits the pre-formatted duration since the SPN is active + * it formats the duration as "HH:MM:SS" or null if the SPN is not enabled. + */ + activeSince$: Observable; + + /** Emits the current status of the SPN */ + status$: Observable; + + /** Emits all map pins */ + _pins$ = new BehaviorSubject([]); + + get pins$(): Observable { + return this._pins$.asObservable(); + } + + pinsMap$ = this.pins$ + .pipe( + filter(allPins => !!allPins.length), + map(allPins => { + const lm = new Map(); + allPins.forEach(pin => lm.set(pin.pin.ID, pin)); + + return lm + }), + share(), + ) + + constructor( + private spnService: SPNService, + private netquery: Netquery, + ) { + this.status$ = this.spnService + .status$ + .pipe( + map(status => !!status ? status.Status : 'disabled'), + distinctUntilChanged() + ); + + // setup the activeSince$ observable that emits every second how long the + // SPN has been active. + this.activeSince$ = combineLatest([ + this.spnService.status$, + interval(1000).pipe(startWith(-1)) + ]).pipe( + map(([status]) => !!status.ConnectedSince ? this.formatActiveSinceDate(status.ConnectedSince) : null), + share(), + ); + + let pinMap = new Map(); + let pinResult: MapPin[] = []; + + // create a stream of pin updates from the SPN service if it is enabled. + this.status$ + .pipe( + switchMap(status => { + if (status !== 'disabled') { + return combineLatest([ + this.spnService.watchPins(), + interval(5000) + .pipe( + startWith(-1), + switchMap(() => this.getPinIDsUsedAsExit()) + ) + ]) + } + return of([[], []]); + }), + map(([pins, exitPinIDs]) => { + const exitPins = new Set(exitPinIDs); + const activePins = new Set(); + const transitPins = new Set(); + const seenPinIDs = new Set(); + + let hasChanges = false; + + pins.forEach(pin => pin.Route?.forEach((hop, index) => { + if (index < pin.Route!.length - 1) { + transitPins.add(hop) + } + + activePins.add(hop); + })); + + pins.forEach(pin => { + // Save Pin ID as seen. + seenPinIDs.add(pin.ID); + + const oldPinModel = pinMap.get(pin.ID); + + // Get states of new model. + const isOffline = pin.States.includes('Offline') || !pin.States.includes('Reachable'); + const isHome = pin.HopDistance === 1; + const isTransit = transitPins.has(pin.ID); + + const isExit = exitPins.has(pin.ID); + const isActive = activePins.has(pin.ID); + const hasIssues = pin.States.includes('ConnectivityIssues'); + + const pinHasChanged = !oldPinModel || oldPinModel.pin !== pin || + oldPinModel.isOffline !== isOffline || oldPinModel.isHome !== isHome || oldPinModel.isTransit !== isTransit || + oldPinModel.isExit !== isExit || oldPinModel.isActive !== isActive || oldPinModel.hasIssues !== hasIssues; + + if (pinHasChanged) { + const newPinModel: MapPin = { + pin: pin, + location: getPinCoords(pin) || UnknownLocation, + entity: (pin.EntityV4 || pin.EntityV6)!, + isExit, + isTransit, + isActive, + isOffline, + isHome, + hasIssues, + } + + pinMap.set(pin.ID, newPinModel); + + hasChanges = true; + } + }) + + for (let key of pinMap.keys()) { + if (!seenPinIDs.has(key)) { + // this pin has been removed + pinMap.delete(key) + hasChanges = true; + } + } + + if (hasChanges) { + pinResult = Array.from(pinMap.values()); + } + + return pinResult; + }), + debounceTime(10), + distinctUntilChanged(), + ) + .subscribe(pins => this._pins$.next(pins)) + } + + getExitPinIDsForProfile(profile: AppProfile) { + return this.netquery + .query({ + select: ['exit_node'], + groupBy: ['exit_node'], + query: { + profile: { $eq: `${profile.Source}/${profile.ID}` }, + } + }, 'map-service-get-exit-pin-ids-for-profile') + .pipe(map(result => result.map(row => row.exit_node!))) + } + + getPinIDsWithActiveSession() { + return this.pins$ + .pipe( + map(result => result.filter(pin => pin.pin.SessionActive).map(pin => pin.pin.ID)) + ) + } + + getPinIDsUsedAsExit() { + return this.netquery + .query({ + select: ['exit_node'], + groupBy: ['exit_node'] + }, 'map-service-get-pins-used-as-exit') + .pipe( + map(result => result.map(row => row.exit_node!)) + ) + } + + getPinIDsWithActiveConnections() { + return this.netquery.query({ + select: ['exit_node'], + groupBy: ['exit_node'], + query: { + active: { $eq: true } + } + }, 'map-service-get-pins-with-connections') + .pipe( + map(activeExitNodes => { + const pins = this._pins$.getValue(); + + const pinIDs = new Set(); + const pinLookupMap = new Map(); + + pins.forEach(p => pinLookupMap.set(p.pin.ID, p)) + + activeExitNodes.map(row => { + const pin = pinLookupMap.get(row.exit_node!); + if (!!pin) { + pin.pin.Route?.forEach(hop => { + pinIDs.add(hop) + }) + } + }) + + return Array.from(pinIDs); + }) + ) + } + + private formatActiveSinceDate(date: string): string { + const d = new Date(date); + const diff = Math.floor((new Date().getTime() - d.getTime()) / 1000); + const hours = Math.floor(diff / 3600); + const minutes = Math.floor((diff - (hours * 3600)) / 60); + const secs = diff - (hours * 3600) - (minutes * 60); + const pad = (d: number) => d < 10 ? `0${d}` : '' + d; + + return `${pad(hours)}:${pad(minutes)}:${pad(secs)}`; + } +} diff --git a/desktop/angular/src/app/pages/spn/node-icon/index.ts b/desktop/angular/src/app/pages/spn/node-icon/index.ts new file mode 100644 index 00000000..715f271d --- /dev/null +++ b/desktop/angular/src/app/pages/spn/node-icon/index.ts @@ -0,0 +1 @@ +export * from './node-icon'; diff --git a/desktop/angular/src/app/pages/spn/node-icon/node-icon.html b/desktop/angular/src/app/pages/spn/node-icon/node-icon.html new file mode 100644 index 00000000..faf8c684 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/node-icon/node-icon.html @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/desktop/angular/src/app/pages/spn/node-icon/node-icon.scss b/desktop/angular/src/app/pages/spn/node-icon/node-icon.scss new file mode 100644 index 00000000..b62c9bac --- /dev/null +++ b/desktop/angular/src/app/pages/spn/node-icon/node-icon.scss @@ -0,0 +1,38 @@ +svg { + + circle, + polygon { + fill: #626262; + stroke: #626262; + stroke-width: 1; + stroke-linejoin: round; + transition: all 200ms linear 0s; + } + + polygon.active, + polygon.exit { + fill: #0376bb; + stroke: #0376bb; + transform: scale(1.15) + } + + circle.active, + circle.exit { + fill: #239215; + stroke: #239215; + transform: scale(1.15) + } + + circle.exit, + polygon.exit { + stroke-width: 2; + } + + circle.exit { + stroke: #30ae20; + } + + polygon.exit { + stroke: #039af4; + } +} diff --git a/desktop/angular/src/app/pages/spn/node-icon/node-icon.ts b/desktop/angular/src/app/pages/spn/node-icon/node-icon.ts new file mode 100644 index 00000000..daad9a15 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/node-icon/node-icon.ts @@ -0,0 +1,44 @@ +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ChangeDetectionStrategy, Component, Input } from "@angular/core"; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'spn-node-icon', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './node-icon.html', + styleUrls: ['./node-icon.scss'], +}) +export class SpnNodeIconComponent { + @Input() + set bySafing(v: any) { + this._bySafing = coerceBooleanProperty(v); + } + get bySafing() { return this._bySafing } + private _bySafing = false; + + @Input() + set isActive(v: any) { + this._isActive = coerceBooleanProperty(v); + } + get isActive() { return this._isActive } + private _isActive = false; + + @Input() + set isExit(v: any) { + this._isExit = coerceBooleanProperty(v); + } + get isExit() { return this._isExit; } + private _isExit = false; + + get nodeClass() { + if (this._isExit) { + return 'exit'; + } + + if (this.isActive) { + return 'active' + } + + return ''; + } +} diff --git a/desktop/angular/src/app/pages/spn/pin-details/index.ts b/desktop/angular/src/app/pages/spn/pin-details/index.ts new file mode 100644 index 00000000..9a9851da --- /dev/null +++ b/desktop/angular/src/app/pages/spn/pin-details/index.ts @@ -0,0 +1 @@ +export * from './pin-details'; diff --git a/desktop/angular/src/app/pages/spn/pin-details/pin-details.html b/desktop/angular/src/app/pages/spn/pin-details/pin-details.html new file mode 100644 index 00000000..8e53befc --- /dev/null +++ b/desktop/angular/src/app/pages/spn/pin-details/pin-details.html @@ -0,0 +1,127 @@ +

+ + {{ pin?.pin?.Name || 'N/A' }} + + + + +

+ + + This SPN Node is run by + + + + {{ pin.pin.VerifiedOwner || 'Community' }} + + +
+ Node is Offline +
+
+ Node has Issues +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID{{ pin.pin.ID }}
Verified Owner +
{{ pin.pin.VerifiedOwner }}
+
First Seen{{ pin.pin.FirstSeen | date:'medium' }}
IPv4 +
+ + + {{ entity.ASOrg }} + ({{ entity.ASN }}) + + + {{ entity.IP || 'N/A' }} + +
+
IPv6 +
+ + + {{ entity.ASOrg }} + ({{ entity.ASN }}) + + + {{ entity.IP || 'N/A' }} + +
+
States +
{{ pin.pin.States.join(", ") }}
+
SessionActive +
{{ pin.pin.SessionActive }}
+
HopDistance +
{{ pin.pin.HopDistance }}
+
Exit Connections +
+
{{ exitConnectionCount }}
+ + + + + + +
+
+
+ + +
+ +
+
+ + + + + +
diff --git a/desktop/angular/src/app/pages/spn/pin-details/pin-details.ts b/desktop/angular/src/app/pages/spn/pin-details/pin-details.ts new file mode 100644 index 00000000..f7e83fae --- /dev/null +++ b/desktop/angular/src/app/pages/spn/pin-details/pin-details.ts @@ -0,0 +1,100 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, OnChanges, OnDestroy, OnInit, Optional, SimpleChanges } from '@angular/core'; +import { Netquery } from '@safing/portmaster-api'; +import { SFNG_DIALOG_REF, SfngDialogRef } from '@safing/ui'; +import { Subscription, forkJoin, map, of, switchMap } from 'rxjs'; +import { LaneModel } from '../pin-list/pin-list'; +import { MapPin, MapService } from './../map.service'; + +@Component({ + templateUrl: './pin-details.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PinDetailsComponent implements OnInit, OnChanges, OnDestroy { + private subscription = Subscription.EMPTY; + + @Input() + mapPinID!: string; + + pin: MapPin | null = null; + + /** Holds all pins this pin has a active connection to */ + connectedPins: LaneModel[] = []; + + /** The number of connections that exit at this pin */ + exitConnectionCount: number = 0; + + constructor( + private mapService: MapService, + private netquery: Netquery, + private cdr: ChangeDetectorRef, + @Optional() @Inject(SFNG_DIALOG_REF) public dialogRef?: SfngDialogRef, + ) { } + + ngOnInit(): void { + // if we got opened via a dialog we get the map pin ID from the dialog data. + if (!!this.dialogRef) { + this.mapPinID = this.dialogRef.data; + } + + this.subscription.unsubscribe(); + + this.subscription = this.mapService + .pins$ + .pipe( + map(pins => { + return [pins.find(p => p.pin.ID === this.mapPinID), pins] as [MapPin, MapPin[]]; + }), + switchMap(([pin, allPins]) => forkJoin({ + pin: of(pin), + allPins: of(allPins), + exitConnections: this.netquery.query({ + select: [ + { $count: { field: '*', as: 'totalCount', } }, + ], + query: { + exit_node: pin.pin.ID, + }, + groupBy: ['exit_node'] + }, 'pin-details-get-connections-per-exit-node') + })) + ) + .subscribe((result) => { + this.pin = result.pin || null; + + const lm = new Map(); + result.allPins.forEach(pin => lm.set(pin.pin.ID, pin)) + + const connectedTo = this.pin?.pin.ConnectedTo || {}; + this.connectedPins = Object.keys(connectedTo) + .map(pinID => { + const pin = lm.get(pinID)!; + return { + ...connectedTo[pinID], + mapPin: pin, + } + }); + + if (result.exitConnections.length) { + // we expect only one row to be returned for the above query. + this.exitConnectionCount = result.exitConnections[0].totalCount; + } else { + this.exitConnectionCount = 0; + } + + this.cdr.markForCheck(); + }) + } + + ngOnChanges(changes: SimpleChanges) { + // if we got rendered directly (without a dialog) we need to + // handle updates to the mapPinID input field by re-loading the + // pin details. We do that by simply re-running ngOnInit + if (!!changes['mapPinID']) { + this.ngOnInit() + } + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } +} diff --git a/desktop/angular/src/app/pages/spn/pin-list/index.ts b/desktop/angular/src/app/pages/spn/pin-list/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/desktop/angular/src/app/pages/spn/pin-list/pin-list.html b/desktop/angular/src/app/pages/spn/pin-list/pin-list.html new file mode 100644 index 00000000..b21077e4 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/pin-list/pin-list.html @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameOperatorUsed AsLatencyCapacityIPv4IPv6
+ + + {{ pin.pin.Name }} + +
+ + + + + {{ pin.pin.VerifiedOwner || 'Community' }} + +
+
+
+ + + + + + + + + + + + + + + + + +
+
+ {{ val.Latency / 1000 / 1000 | number:'1.0-2' }} ms + + {{ val.Capacity / 1000 / 1000 | number:'1.0-2' }} Mbit/s + {{ pin.pin.EntityV4?.IP || 'N/A' }}{{ pin.pin.EntityV6?.IP || 'N/A' }} + + + +
diff --git a/desktop/angular/src/app/pages/spn/pin-list/pin-list.ts b/desktop/angular/src/app/pages/spn/pin-list/pin-list.ts new file mode 100644 index 00000000..6f3eeedf --- /dev/null +++ b/desktop/angular/src/app/pages/spn/pin-list/pin-list.ts @@ -0,0 +1,87 @@ +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, TrackByFunction } from '@angular/core'; +import { Lane } from '@safing/portmaster-api'; +import { take } from 'rxjs/operators'; +import { MapPin } from '../map.service'; +import { MapService } from './../map.service'; + +export interface LaneModel extends Lane { + mapPin: MapPin; +} + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'spn-pin-list', + templateUrl: './pin-list.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SpnPinListComponent { + @Input() + set allowHover(v: any) { + this._allowHover = coerceBooleanProperty(v); + } + get allowHover() { return this._allowHover } + private _allowHover = true; + + @Input() + set allowClick(v: any) { + this._allowClick = coerceBooleanProperty(v); + } + get allowClick() { return this._allowClick } + private _allowClick = true; + + @Input() + set pins(pins: (string | MapPin | LaneModel)[]) { + this.mapService + .pinsMap$ + .pipe(take(1)) + .subscribe(allPins => { + this.lanes = null; + + this._pins = (pins || []).map(idOrPin => { + if (typeof idOrPin === 'string') { + return allPins.get(idOrPin)!; + } + + if ('mapPin' in idOrPin) { // LaneModel + if (this.lanes === null) { + this.lanes = new Map(); + } + + this.lanes.set(idOrPin.HubID, { + Capacity: idOrPin.Capacity, + Latency: idOrPin.Latency, + }) + + return idOrPin.mapPin; + } + + return idOrPin; // MapPin + }) + + this.cdr.markForCheck(); + }) + } + get pins(): MapPin[] { + return this._pins; + } + private _pins: MapPin[] = []; + + /** If we got LaneModel in @Input() pins than this will contain a map with the capacity/latency */ + lanes: Map> | null = null; + + /** Emits the ID of the pin that got hovered, null if the mouse left a pin */ + @Output() + pinHover = new EventEmitter(); + + @Output() + pinClick = new EventEmitter(); + + /** @private - A {@link TrackByFunction} for all pins available in this country */ + trackPin: TrackByFunction = (_: number, pin: MapPin) => pin.pin.ID; + + constructor( + private mapService: MapService, + private cdr: ChangeDetectorRef + ) { } +} diff --git a/desktop/angular/src/app/pages/spn/pin-overlay/index.ts b/desktop/angular/src/app/pages/spn/pin-overlay/index.ts new file mode 100644 index 00000000..620c76d3 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/pin-overlay/index.ts @@ -0,0 +1 @@ +export * from './pin-overlay'; diff --git a/desktop/angular/src/app/pages/spn/pin-overlay/pin-overlay.html b/desktop/angular/src/app/pages/spn/pin-overlay/pin-overlay.html new file mode 100644 index 00000000..4bcd2f4c --- /dev/null +++ b/desktop/angular/src/app/pages/spn/pin-overlay/pin-overlay.html @@ -0,0 +1,117 @@ +
+
+ + Show Details + Show exit connections + Copy Node ID + + + + + {{ mapPin.pin.Name }} + + + + + + + + + + + + + + + + + + +
+
+ IPv4 + {{ mapPin.pin.EntityV4?.IP || 'N/A' }} +
+
+ IPv6 + {{ mapPin.pin.EntityV6?.IP || 'N/A' }} +
+
+ Run By + + + + + + + {{ mapPin.pin.VerifiedOwner || 'Community' }} + +
+
+ Used As + +
+ + + + + + + Home Node + + + + + + + + + + Exit Node + + + + + + + + + + Transit Node + + + + +
+
+
+ + + + + + + + + diff --git a/desktop/angular/src/app/pages/spn/pin-overlay/pin-overlay.scss b/desktop/angular/src/app/pages/spn/pin-overlay/pin-overlay.scss new file mode 100644 index 00000000..68c4ea1f --- /dev/null +++ b/desktop/angular/src/app/pages/spn/pin-overlay/pin-overlay.scss @@ -0,0 +1,4 @@ +:host { + min-width: 220px; + display: block; +} diff --git a/desktop/angular/src/app/pages/spn/pin-overlay/pin-overlay.ts b/desktop/angular/src/app/pages/spn/pin-overlay/pin-overlay.ts new file mode 100644 index 00000000..00122703 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/pin-overlay/pin-overlay.ts @@ -0,0 +1,190 @@ +import { AnimationEvent, animate, keyframes, style, transition, trigger } from '@angular/animations'; +import { CdkDrag, CdkDragHandle, CdkDragRelease } from '@angular/cdk/drag-drop'; +import { Overlay, OverlayRef, PositionStrategy } from '@angular/cdk/overlay'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostListener, Inject, Input, OnInit, Output, ViewChild, inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { SfngDialogService } from '@safing/ui'; +import { PinDetailsComponent } from '../pin-details'; +import { MapOverlay, Path } from '../spn-page'; +import { ActionIndicatorService } from './../../../shared/action-indicator/action-indicator.service'; +import { MapPin } from './../map.service'; +import { OVERLAY_REF } from './../utils'; +import { INTEGRATION_SERVICE } from 'src/app/integration'; + +export interface PinOverlayHoverEvent { + type: 'enter' | 'leave'; + pinID: string; +} + +@Component({ + templateUrl: './pin-overlay.html', + styleUrls: [ + './pin-overlay.scss' + ], + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + trigger('moveIn', [ + transition(':enter', [ + style({ transform: 'scale(0)', transformOrigin: 'top left' }), + animate('200ms {{ delay }}ms cubic-bezier(0, 0, 0.2, 1)', + keyframes([ + style({ transform: 'scaleX(1) scaleY(0.1)', transformOrigin: 'top left', offset: 0.3 }), + style({ transform: 'scaleX(1) scaleY(1)', transformOrigin: 'top left', offset: 0.8 }), + ]) + ) + ], { params: { delay: "0" } }), + transition(':leave', [ + style({ transform: 'scale(1)', opacity: 1, transformOrigin: 'top left' }), + animate('500ms cubic-bezier(0, 0, 0.2, 1)', + keyframes([ + style({ transform: 'scaleX(1) scaleY(0.1)', opacity: 0.5, transformOrigin: 'top left', offset: 0.3 }), + style({ transform: 'scaleX(0) scaleY(0)', opacity: 0, transformOrigin: 'top left', offset: 0.8 }), + ]) + ) + ]) + ]) + ] +}) +export class PinOverlayComponent implements OnInit { + private readonly integration = inject(INTEGRATION_SERVICE); + + @Input() + mapPin!: MapPin; + + @Input() + routeHome?: Path; + + @Input() + additionalPaths?: Path[] = []; + + @Input() + delay: number = 0; + + @Output() + afterDispose = new EventEmitter(); + + @Output() + overlayHover = new EventEmitter(); + + @ViewChild(CdkDrag) + dragContainer!: CdkDrag; + + @ViewChild(CdkDragHandle) + dragHandle!: CdkDragHandle; + + showContent = false; + + /** Indicates whether or not the pin overlay has been moved by the user */ + hasBeenMoved = false; + + private oldPositionStrategy?: PositionStrategy; + + @HostListener('mouseenter') + onHostElementMouseEnter(event: MouseEvent) { + this.overlayHover.next({ + type: 'enter', + pinID: this.mapPin.pin.ID + }) + + this.containerClass = ''; + } + + @HostListener('mouseleave') + onHostElementMouseLeave(event: MouseEvent) { + this.overlayHover.next({ + type: 'leave', + pinID: this.mapPin.pin.ID + }) + + this.containerClass = 'bg-opacity-90' + } + + /** on double-click, restore the old pin overlay position (before being initialy dragged by the user) */ + onDragDblClick() { + if (!!this.oldPositionStrategy) { + this.overlayRef.updatePositionStrategy(this.oldPositionStrategy); + this.overlayRef.updatePosition(); + this.hasBeenMoved = false; + } + } + + onDragStart() { + this.containerClass = 'outline' + } + + openPinDetails() { + this.dialog.create(PinDetailsComponent, { + data: this.mapPin.pin.ID, + autoclose: true, + backdrop: false, + dragable: true, + }) + } + + onDragRelease(event: CdkDragRelease) { + if (!this.dragContainer || !this.overlayRef.hostElement || !this.overlayRef.hostElement.parentElement) { + return; + } + + const bbox = this.dragContainer.element.nativeElement.getBoundingClientRect(); + const parent = this.overlayRef.hostElement.parentElement!.getBoundingClientRect(); + + if (!this.oldPositionStrategy) { + this.oldPositionStrategy = this.overlayRef.getConfig().positionStrategy; + } + + this.containerClass = ''; + + this.dragContainer.reset() + + this.overlayRef.updatePositionStrategy( + this.overlay.position() + .global() + .top((bbox.top - parent.top) + 'px') + .left((bbox.left - parent.left) + 'px') + ); + + this.hasBeenMoved = true; + } + + onAnimationComplete(event: AnimationEvent) { + if (event.toState === 'void') { + this.afterDispose.next(this.mapPin.pin.ID) + this.overlayRef.dispose(); + } + } + + containerClass = ''; + + constructor( + @Inject(OVERLAY_REF) public readonly overlayRef: OverlayRef, + @Inject(MapOverlay) public overlay: Overlay, + private dialog: SfngDialogService, + private actionIndicator: ActionIndicatorService, + private router: Router, + private cdr: ChangeDetectorRef, + ) { } + + ngOnInit(): void { + this.showContent = true; + this.cdr.markForCheck(); + } + + disposeOverlay() { + this.showContent = false; + this.cdr.markForCheck(); + } + + showExitConnections() { + this.router.navigate(['/monitor'], { + queryParams: { + q: 'exit_node:' + this.mapPin.pin.ID + } + }) + } + + async copyNodeID() { + await this.integration.writeToClipboard(this.mapPin?.pin.ID) + this.actionIndicator.success("Copied to Clipboard") + } +} diff --git a/desktop/angular/src/app/pages/spn/pin-route/index.ts b/desktop/angular/src/app/pages/spn/pin-route/index.ts new file mode 100644 index 00000000..f97ea758 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/pin-route/index.ts @@ -0,0 +1 @@ +export * from './pin-route'; diff --git a/desktop/angular/src/app/pages/spn/pin-route/pin-route.html b/desktop/angular/src/app/pages/spn/pin-route/pin-route.html new file mode 100644 index 00000000..1927c5cd --- /dev/null +++ b/desktop/angular/src/app/pages/spn/pin-route/pin-route.html @@ -0,0 +1,53 @@ + +
+ +
+
    +
  • + + + + + Your Device + +
  • + +
  • + + + + + + {{ node.entity.Country || 'No Location' }} + {{ node.entity.IP || '' + }} + Home + Exit +
    {{ node.pin.Name }} + + + by + + {{ node.pin.VerifiedOwner || 'Community' }} + +
    + +
    AS{{ node.entity.ASN }} - {{ node.entity.ASOrg || + 'AS Organization not in DB' + }}
    + +
    {{ node.pin.ID }}
    +
  • + +
  • + + + + + + Destination + + +
  • +
+
diff --git a/desktop/angular/src/app/pages/spn/pin-route/pin-route.scss b/desktop/angular/src/app/pages/spn/pin-route/pin-route.scss new file mode 100644 index 00000000..2c66a8ee --- /dev/null +++ b/desktop/angular/src/app/pages/spn/pin-route/pin-route.scss @@ -0,0 +1,67 @@ +.tunnel-path { + position: relative; + + .line { + position: absolute; + top: 10px; + bottom: 10px; + left: 8px; + width: 1px; + background-color: rgba(255, 255, 255, 0.1); + } + + .node-tag { + border-radius: 1px solid rgba(255, 255, 255, 0.2); + background-color: rgba(255, 255, 255, 0.1); + padding: 2px; + font-size: 85%; + border-radius: 2px; + transform: scale(0.85); + display: inline-block; + } + + ul { + position: relative; + padding-left: 20px; + + li:not(:last-of-type) { + padding-bottom: 0.35rem; + } + + .ip { + margin-left: 0.35rem; + } + + .hop-icon { + display: inline-block; + margin-left: -17px; + margin-right: 4px; + font-weight: 400; + + &.country { + margin-left: -20px; + } + } + + .hop-title { + margin-right: 2px; + } + + .country { + display: inline-block; + margin-left: -20px; + margin-right: 4px; + + &.unknown { + height: 14px; + width: 16px; + position: relative; + top: 3px; + border: 1px solid rgba(0, 0, 0, 0.25); + opacity: 0.5; + border-radius: 3px; + @apply bg-buttons-icon; + } + } + } +} diff --git a/desktop/angular/src/app/pages/spn/pin-route/pin-route.ts b/desktop/angular/src/app/pages/spn/pin-route/pin-route.ts new file mode 100644 index 00000000..d862619d --- /dev/null +++ b/desktop/angular/src/app/pages/spn/pin-route/pin-route.ts @@ -0,0 +1,46 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input } from "@angular/core"; +import { TunnelNode } from "@safing/portmaster-api"; +import { take } from 'rxjs'; +import { MapPin, MapService } from './../map.service'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'sfng-spn-pin-route', + templateUrl: './pin-route.html', + styleUrls: ['./pin-route.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SpnPinRouteComponent { + @Input() + set route(path: (string | MapPin | TunnelNode)[] | null) { + this.mapService + .pinsMap$ + .pipe( + take(1), + ) + .subscribe(lm => { + this._route = (path || []).map(idOrPin => { + if (typeof idOrPin === 'string') { + return lm.get(idOrPin)!; + } + + if ('ID' in idOrPin) { // TunnelNode + return lm.get(idOrPin.ID)! + } + + return idOrPin; + }); + + this.cdr.markForCheck(); + }) + } + get route(): MapPin[] { + return this._route + } + private _route: MapPin[] = []; + + constructor( + private mapService: MapService, + private cdr: ChangeDetectorRef, + ) { } +} diff --git a/desktop/angular/src/app/pages/spn/spn-feature-carousel/index.ts b/desktop/angular/src/app/pages/spn/spn-feature-carousel/index.ts new file mode 100644 index 00000000..d07cc9e1 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/spn-feature-carousel/index.ts @@ -0,0 +1 @@ +export * from './spn-feature-carousel'; diff --git a/desktop/angular/src/app/pages/spn/spn-feature-carousel/spn-feature-carousel.html b/desktop/angular/src/app/pages/spn/spn-feature-carousel/spn-feature-carousel.html new file mode 100644 index 00000000..b73683f4 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/spn-feature-carousel/spn-feature-carousel.html @@ -0,0 +1,274 @@ +
+ + + + + + +
+
+

Get + Multiple Identities for Each App

+ + Automatically get a vast amount of identities (IP addresses). The SPN calculates an individual path for + every + connection through the privacy network. Spread your connections across the globe, without any effort. + +
+ +
+
+ + + + +
+
+

Easily Adjust Your Privacy

+ + SPN just works and does the heavy lifting for you. But of course you can easily configure the settings, so + it fits your needs: Exclude certain apps and domains from the SPN. Or never exit in specific countries. And + so much more... + +
+ +
+
+ + +
+
+

Built from Scratch, for Your Privacy

+ + SPN is built from the ground up. Privacy is cooked right into it. Inspired by Tor, it comes with onion + routing and state of the art encryption. Fully open source so all our claims can be validated. + +
+ +
+
+ + +
+
+

Bye Bye, VPNs

+ + VPN technology was NOT built for user privacy, but for company security. Because of that, you can only trust + a VPN provider's policy - and many have been caught abusing user data. Honestly, the best way forward: just + stop paying for outdated technology. + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Most VPNs +
+ Read Comparison Blog +
+
SPNTor
Multiple Identities (simultaneous) + + + + + +
Individual Apps Settings + + + + + +
Easy Setup + + + + Browser Only
Availabilty +
+ + + + +
+
+
+ + +
+
+
+ + + + +
+
Open Source + + + + + +
Built for Privacy + + + + + +
+
+
+
+ + + + + +
+ +
+ +
diff --git a/desktop/angular/src/app/pages/spn/spn-feature-carousel/spn-feature-carousel.scss b/desktop/angular/src/app/pages/spn/spn-feature-carousel/spn-feature-carousel.scss new file mode 100644 index 00000000..7ffa92a2 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/spn-feature-carousel/spn-feature-carousel.scss @@ -0,0 +1,62 @@ +:host { + @apply flex flex-col gap-2 justify-center items-center relative; +} + +section { + @apply flex flex-row items-start gap-4 justify-evenly text-background; + + &.reverse { + @apply flex-row-reverse + } + + &>div { + @apply flex flex-col w-1/3 gap-6; + + span { + @apply text-base break-normal text-background text-opacity-80; + } + + h1, + h1>span { + @apply text-2xl font-semibold break-normal md:text-3xl lg:text-4xl xl:text-5xl text-background; + + } + + h1>span { + &.text-blue { + color: theme('colors.blue.DEFAULT') !important; + } + } + } + + img { + position: relative; + max-width: 50%; + } + + table { + @apply mb-12; + + th { + @apply text-base; + } + + td { + @apply text-center p-2 leading-6; + } + + tr>td:first-of-type { + @apply text-left p-2 font-semibold text-base whitespace-nowrap; + } + } +} + +::ng-deep { + spn-feature-carousel { + sfng-tab-outlet { + &>div { + overflow: visible !important; + } + } + } +} diff --git a/desktop/angular/src/app/pages/spn/spn-feature-carousel/spn-feature-carousel.ts b/desktop/angular/src/app/pages/spn/spn-feature-carousel/spn-feature-carousel.ts new file mode 100644 index 00000000..e68edbb3 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/spn-feature-carousel/spn-feature-carousel.ts @@ -0,0 +1,83 @@ +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, OnDestroy, QueryList, ViewChild, ViewChildren } from "@angular/core"; +import { SfngTabComponent, SfngTabGroupComponent } from '@safing/ui'; +import { filter, interval, startWith, Subscription } from 'rxjs'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'spn-feature-carousel', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './spn-feature-carousel.html', + styleUrls: [ + './spn-feature-carousel.scss' + ] +}) +export class SPNFeatureCarouselComponent implements AfterViewInit, OnDestroy { + private sub: Subscription = Subscription.EMPTY; + + pause = false; + currentIndex = -1; + + @HostListener('mouseenter') + onMouseEnter() { + this.pause = true + } + + @HostListener('mouseleave') + onMouseLeave() { + this.pause = false; + } + + /** A list of all carousel templates */ + @ViewChildren(SfngTabComponent) + carousel!: QueryList; + + @ViewChild(SfngTabGroupComponent) + tabGroup!: SfngTabGroupComponent; + + constructor( + private cdr: ChangeDetectorRef + ) { } + + ngAfterViewInit(): void { + this.sub = interval(5000) + .pipe( + startWith(-1), + filter(() => !this.pause), + ) + .subscribe(() => { + this.openTab(this.currentIndex + 1, 'left') + }) + } + + ngOnDestroy(): void { + this.sub.unsubscribe() + } + + openTab(idx: number, direction?: 'left' | 'right') { + // force animation to circle if we go before the first + // or after the last one. + if (idx < 0) { + idx = this.carousel.length - 1; + direction = 'right' + } + if (idx >= this.carousel.length) { + direction = 'left' + } + + this.currentIndex = idx % this.carousel.length; + this.tabGroup.activateTab(this.currentIndex, direction)!; + this.cdr.markForCheck(); + } + + showNext() { + this.sub.unsubscribe() + + this.openTab(this.currentIndex + 1) + } + + showPrev() { + this.sub.unsubscribe() + + this.openTab(this.currentIndex - 1) + } +} diff --git a/desktop/angular/src/app/pages/spn/spn-page.html b/desktop/angular/src/app/pages/spn/spn-page.html new file mode 100644 index 00000000..28a61450 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/spn-page.html @@ -0,0 +1,102 @@ + +
+
+ +
+ + Loading data, please wait ... +
+
+
+ +
+
+ + + + +
+ + + + + + Pricing + +
+
+
+
+ + +
+ + +
+ +
+ + + + + + + +
+ + + + Pro Tip: + +
+ + +
+
+ + + + Hold +
CTRL
key and click a node on the map to immediately open the node details dialog. +
+ + + Hold +
SHIFT
key to open more than one node overlay when clicking the node icon. +
+ + + To keep node overlays open move them using + + + . Double click to revert the overlay position on the map. + + + + Click on a country to get more information about all nodes in that country and a list of Apps that use nodes in the + country as an identity. + +
diff --git a/desktop/angular/src/app/pages/spn/spn-page.scss b/desktop/angular/src/app/pages/spn/spn-page.scss new file mode 100644 index 00000000..40441ef4 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/spn-page.scss @@ -0,0 +1,143 @@ +:host { + @apply flex flex-row w-full h-full justify-items-stretch items-stretch relative; +} + +.text-info-red { + color: theme("colors.info.red"); +} + +.network-status-dialog { + width: 50vw; + height: 50vh; + min-height: 300px; + min-width: 400px; + padding: 12px; + overflow: auto; + display: flex; + flex-direction: column; + + .issue { + flex-grow: 1; + } + + .issue-list { + width: 100% !important; + flex-grow: 1; + + ul { + overflow: auto; + } + } + + .issue.expanded { + background-color: var(--button-light) !important; + } + + .body { + background-color: var(--cards-primary) !important; + } +} + +.connect-button { + + &.spn-connected { + @apply bg-info-blue; + } + + &.spn-connecting { + @apply bg-info-blue; + } + + &.spn-failed { + @apply bg-info-red; + } + + &:hover { + @apply bg-info-blue opacity-75; + } +} + +.table { + @apply w-full font-normal; + + &>div { + @apply text-xs border-buttons-dark flex flex-row justify-between py-1; + + &:not(:last-child) { + @apply border-b; + } + + span:first-child { + @apply text-tertiary; + } + + span:last-child { + @apply text-primary; + } + } +} + + +table tr:nth-child(odd) { + background: none; +} + + +.tunnel-path { + position: relative; + + .line { + position: absolute; + top: 10px; + bottom: 10px; + left: 8px; + width: 1px; + background-color: rgba(255, 255, 255, 0.1); + } + + + ul { + position: relative; + padding-left: 20px; + + li:not(:last-of-type) { + padding-bottom: 0.35rem; + } + + .ip { + margin-left: 0.35rem; + } + + .hop-icon { + display: inline-block; + margin-left: -17px; + margin-right: 4px; + font-weight: 400; + + &.country { + margin-left: -20px; + } + } + + .hop-title { + margin-right: 2px; + } + + .country { + display: inline-block; + margin-left: -20px; + margin-right: 4px; + + &.unknown { + height: 14px; + width: 16px; + position: relative; + top: 3px; + border: 1px solid rgba(0, 0, 0, 0.25); + opacity: 0.5; + border-radius: 3px; + @apply bg-buttons-icon; + } + } + } +} diff --git a/desktop/angular/src/app/pages/spn/spn-page.ts b/desktop/angular/src/app/pages/spn/spn-page.ts new file mode 100644 index 00000000..992dbe19 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/spn-page.ts @@ -0,0 +1,1012 @@ +import { coerceElement } from "@angular/cdk/coercion"; +import { Overlay, OverlayContainer } from "@angular/cdk/overlay"; +import { ComponentPortal } from '@angular/cdk/portal'; +import { HttpClient } from '@angular/common/http'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentRef, DestroyRef, ElementRef, Inject, Injectable, InjectionToken, Injector, OnDestroy, OnInit, QueryList, TemplateRef, ViewChild, ViewChildren, forwardRef, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute, ParamMap, Router } from "@angular/router"; +import { AppProfile, ConfigService, Connection, ExpertiseLevel, FeatureID, Netquery, PORTMASTER_HTTP_API_ENDPOINT, PortapiService, SPNService, SPNStatus, UserProfile } from "@safing/portmaster-api"; +import { SfngDialogService } from "@safing/ui"; +import { Line as D3Line, Selection, interpolateString, line, select } from 'd3'; +import { BehaviorSubject, Observable, Subscription, combineLatest, interval, of } from "rxjs"; +import { catchError, debounceTime, map, mergeMap, share, startWith, switchMap, take, takeUntil, withLatestFrom } from "rxjs/operators"; +import { fadeInAnimation, fadeInListAnimation, fadeOutAnimation } from "src/app/shared/animations"; +import { ExpertiseService } from "src/app/shared/expertise/expertise.service"; +import { SPNAccountDetailsComponent } from "src/app/shared/spn-account-details"; +import { CountryDetailsComponent } from "./country-details"; +import { CountryEvent, MAP_HANDLER, MapRef, MapRendererComponent } from "./map-renderer/map-renderer"; +import { MapPin, MapService } from "./map.service"; +import { PinDetailsComponent } from "./pin-details"; +import { PinOverlayComponent } from "./pin-overlay"; +import { OVERLAY_REF } from './utils'; + +export const MapOverlay = new InjectionToken('MAP_OVERLAY') + +export type PinGroup = Selection; +export type LaneGroup = Selection; + +export interface Path { + id: string; + points: (MapPin | [number, number])[]; + attributes?: { + [key: string]: string; + } +} + +export interface PinEvent { + event?: MouseEvent; + mapPin: MapPin; +} + + +/** + * A custom class that implements the OverlayContainer interface of CDK. This + * is used so we can configure a custom container element that will hold all overlays created + * by the map component. This way the overlays will be bound to the map container and not overflow + * the sidebar or other overlays that are created by the "root" app. + */ +@Injectable() +class MapOverlayContainer { + private _overlayContainer?: HTMLElement; + + setOverlayContainer(element: ElementRef | HTMLElement) { + this._overlayContainer = coerceElement(element); + } + + getContainerElement(): HTMLElement { + if (!this._overlayContainer) { + throw new Error("Overlay container element not initialized. Call setOverlayContainer first.") + } + + return this._overlayContainer; + } +} + +@Component({ + templateUrl: './spn-page.html', + styleUrls: ['./spn-page.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + MapOverlayContainer, + { provide: MapOverlay, useClass: Overlay }, + { provide: OverlayContainer, useExisting: MapOverlayContainer }, + { provide: MAP_HANDLER, useExisting: forwardRef(() => SpnPageComponent), multi: true } + ], + animations: [ + fadeInListAnimation, + fadeInAnimation, + fadeOutAnimation + ] +}) +export class SpnPageComponent implements OnInit, OnDestroy, AfterViewInit { + private destroyRef = inject(DestroyRef); + + private countryDebounceTimer: any | null = null; + + /** a list of opened country details. required to close them on destry */ + private openedCountryDetails: CountryDetailsComponent[] = []; + + readonly featureID = FeatureID.SPN; + + paths: Path[] = []; + + @ViewChild('overlayContainer', { static: true, read: ElementRef }) + overlayContainer!: ElementRef; + + @ViewChild(MapRendererComponent, { static: true }) + mapRenderer!: MapRendererComponent; + + @ViewChild('accountDetails', { read: TemplateRef, static: true }) + accountDetails: TemplateRef | null = null; + + /** A list of pro-tip templates in our view */ + @ViewChildren('proTip', { read: TemplateRef }) + proTipTemplates!: QueryList>; + + /** The selected pro-tip template */ + proTipTemplate: TemplateRef | null = null; + + /** currentUser holds the current SPN user profile if any */ + currentUser: UserProfile | null = null; + + /** An observable that emits all active processes. */ + activeProfiles$: Observable; + + /** Whether or not we are still waiting for all data in order to satisfy a "show process/pin" request by query-params */ + loading = true; + + /** a list of currently selected pins */ + selectedPins: PinOverlayComponent[] = []; + + /** the currently hovered country, if any */ + hoveredCountry: { + countryName: string; + countryCode: string; + } | null = null; + + liveMode = false; + liveModePaths: Path[] = []; + + private liveModeSubscription = Subscription.EMPTY; + + /** + * spnStatusTranslation translates the spn status to the text that is displayed + * at the view + */ + readonly spnStatusTranslation: Readonly> = { + connected: 'Connected', + connecting: 'Connecting', + disabled: 'Disabled', + failed: 'Failure' + } + + + private mapRef: MapRef | null = null; + private lineFunc: D3Line<(MapPin | [number, number])> | null = null; + private highlightedPins = new Set(); + + registerMap(ref: MapRef) { + this.mapRef = ref; + + ref.onMapReady(() => { + // we want to have straight lines between our hubs so we use a custom + // path function that updates x and y coordinates based on the mercator projection + // without, points will no be at the correct geo-coordinates. + this.lineFunc = line() + .x(d => { + if (Array.isArray(d)) { + return this.mapRef!.projection([d[0], d[1]])![0]; + } + return this.mapRef!.projection([d.location.Longitude, d.location.Latitude])![0]; + }) + .y(d => { + if (Array.isArray(d)) { + return this.mapRef!.projection([d[0], d[1]])![1]; + } + return this.mapRef!.projection([d.location.Longitude, d.location.Latitude])![1]; + }) + + this.mapRef!.root.append('g').attr('id', 'line-group') + this.mapRef!.root.append('g').attr('id', 'pin-group') + + if (this.mapService._pins$.getValue().length > 0) { + this.renderPins(this.mapService._pins$.getValue()) + } + }) + + ref.onCountryClick(event => this.onCountryClick(event)) + ref.onCountryHover(event => this.onCountryHover(event)) + ref.onZoomPan(() => this.onZoomAndPan()) + } + + unregisterMap(ref: MapRef) { + this.mapRef = null; + this.lineFunc = null; + } + + constructor( + private configService: ConfigService, + private spnService: SPNService, + private netquery: Netquery, + private expertiseService: ExpertiseService, + private router: Router, + private route: ActivatedRoute, + private portapi: PortapiService, + @Inject(PORTMASTER_HTTP_API_ENDPOINT) private httpAPI: string, + private http: HttpClient, + public mapService: MapService, + @Inject(MapOverlay) private mapOverlay: Overlay, + private dialog: SfngDialogService, + private overlayContainerService: MapOverlayContainer, + private cdr: ChangeDetectorRef, + private injector: Injector, + ) { + this.activeProfiles$ = interval(5000) + .pipe( + startWith(-1), + switchMap(() => this.netquery.getActiveProfiles()), + share({ connector: () => new BehaviorSubject([]) }) + ) + } + + ngAfterViewInit() { + // configure our custom overlay container + this.overlayContainerService.setOverlayContainer(this.overlayContainer); + + // Select a random "Pro-Tip" template and run change detection + this.proTipTemplate = this.proTipTemplates.get(Math.floor(Math.random() * this.proTipTemplates.length)) || null; + this.cdr.detectChanges(); + } + + openAccountDetails() { + this.dialog.create(SPNAccountDetailsComponent, { + autoclose: true, + backdrop: 'light' + }) + } + + ngOnInit() { + this.spnService + .profile$ + .pipe( + takeUntilDestroyed(this.destroyRef), + catchError(() => of(null)) + ) + .subscribe((user: UserProfile | null) => { + if (user?.state !== '') { + this.currentUser = user || null; + } else { + this.currentUser = null; + } + + this.cdr.markForCheck(); + }) + + let previousQueryMap: ParamMap | null = null; + + combineLatest([ + this.route.queryParamMap, + this.mapService.pins$, + this.activeProfiles$, + ]) + .pipe( + takeUntilDestroyed(this.destroyRef), + ).subscribe(([params, pins, profiles]) => { + if (params !== previousQueryMap) { + const app = params.get("app") + if (!!app) { + const profile = profiles.find(p => `${p.Source}/${p.ID}` === app); + if (!!profile) { + const pinID = params.get("pin") + const pin = pins.find(p => p.pin.ID === pinID); + + this.selectGroup(profile, pin) + } + } + + previousQueryMap = params; + } + + this.renderPins(pins); + + // we're done with everything now. + this.loading = false; + }) + + } + + toggleLiveMode(enabled: boolean) { + this.liveMode = enabled; + + if (!enabled) { + this.liveModeSubscription.unsubscribe(); + this.liveModePaths = []; + this.updatePaths([]); + this.cdr.markForCheck(); + + return; + } + + this.liveModeSubscription = this.portapi.watchAll("network:tree") + .pipe( + withLatestFrom(this.mapService.pinsMap$), + takeUntilDestroyed(this.destroyRef), + debounceTime(100), + ) + .subscribe(([connections, mapPins]) => { + connections = connections.filter(conn => conn.Ended === 0 && !!conn.TunnelContext); + + this.liveModePaths = connections.map(conn => { + const points: (MapPin | [number, number])[] = conn.TunnelContext!.Path.map(hop => mapPins.get(hop.ID)!) + + if (!!conn.Entity.Coordinates) { + points.push([conn.Entity.Coordinates.Longitude, conn.Entity.Coordinates.Latitude]) + } + + return { + id: conn.Entity.Domain || conn.ID, + points: points, + attributes: { + 'is-live': 'true', + 'is-encrypted': `${conn.Encrypted}` + } + } + }) + + this.updatePaths([]) + this.cdr.markForCheck(); + }) + } + + /** + * Toggle the spn/enable setting. This does NOT update the view as that + * will happen as soon as we get an update from the db qsub. + * + * @private - template only + */ + toggleSPN() { + this.configService.get('spn/enable') + .pipe( + map(setting => setting.Value ?? setting.DefaultValue), + mergeMap(active => this.configService.save('spn/enable', !active)) + ) + .subscribe() + } + + /** + * Select one or more pins by ID. If shift key is hold then all currently + * selected pin overlays will be cleared before selecting the new ones. + */ + private selectPins(event: MouseEvent | undefined, pinIDs: Observable) { + combineLatest([ + this.mapService.pins$, + pinIDs, + ]) + .pipe(take(1)) + .subscribe(([allPins, pinIDs]) => { + if (event?.shiftKey !== true) { + this.selectedPins + .filter(overlay => !overlay.hasBeenMoved) + .forEach(selected => selected.disposeOverlay()) + } + + pinIDs + .filter(id => !this.selectedPins.find(selectedPin => selectedPin.mapPin.pin.ID === id)) + .map(id => allPins.find(pin => pin.pin.ID === id)) + .filter(mapPin => !!mapPin) + .forEach(mapPin => this.onPinClick({ + mapPin: mapPin!, + })); + }) + } + + /** + * Select all pins that are used for transit. + * + * @private - template only + */ + selectTransitNodes(event: MouseEvent) { + this.selectPins(event, this.mapService.getPinIDsWithActiveSession()) + } + + /** + * Select all pins that are used as an exit hub. + * + * @private - template only + */ + selectExitNodes(event: MouseEvent) { + this.selectPins(event, this.mapService.getPinIDsUsedAsExit()) + } + + /** + * Select all pins that currently host alive connections. + * + * @private - template only + */ + selectNodesWithAliveConnections(event: MouseEvent) { + this.selectPins(event, this.mapService.getPinIDsWithActiveConnections()) + } + + navigateToMonitor(process: AppProfile) { + this.router.navigate(['/app', process.Source, process.ID]) + } + + ngOnDestroy() { + this.openedCountryDetails.forEach(cmp => cmp.dialogRef!.close()); + } + + onZoomAndPan() { + this.updateOverlayPositions(); + + if (this.mapRef) { + this.mapRef.root + .select('#lines-group') + .selectAll('path') + .attr('d', d => this.lineFunc!(d.points)) + + this.mapRef.root + .select("#pin-group") + .selectAll('g') + .attr('transform', d => `translate(${this.mapRef!.projection([d.location.Longitude, d.location.Latitude])})`) + } + + this.cdr.markForCheck(); + } + + private createPinOverlay(pinEvent: PinEvent, lm: Map): PinOverlayComponent { + const paths = this.getRouteHome(pinEvent.mapPin, lm, false) + const overlayBoundingRect = this.overlayContainer.nativeElement.getBoundingClientRect(); + const target = pinEvent.event?.target || this.getPinElem(pinEvent.mapPin.pin.ID)?.children[0]; + let delay = 0; + if (paths.length > 0) { + delay = paths[0].points.length * MapRendererComponent.LineAnimationDuration; + } + + const overlayRef = this.mapOverlay.create({ + positionStrategy: this.mapOverlay.position() + .flexibleConnectedTo(new ElementRef(target)) + .withDefaultOffsetY(-overlayBoundingRect.y - 10) + .withDefaultOffsetX(-overlayBoundingRect.x + 20) + .withPositions([ + { + overlayX: 'start', + overlayY: 'top', + originX: 'start', + originY: 'top' + } + ]), + scrollStrategy: this.mapOverlay.scrollStrategies.reposition(), + }) + + const injector = Injector.create({ + providers: [ + { + provide: OVERLAY_REF, + useValue: overlayRef, + } + ], + parent: this.injector + }) + + + const pinOverlay = overlayRef.attach( + new ComponentPortal(PinOverlayComponent, undefined, injector) + ).instance; + + pinOverlay.delay = delay; + pinOverlay.mapPin = pinEvent.mapPin; + if (paths.length > 0) { + pinOverlay.routeHome = { + ...(paths[0]), + } + pinOverlay.additionalPaths = paths.slice(1); + } + + return pinOverlay; + } + + + private openPinDetails(id: string) { + this.dialog.create(PinDetailsComponent, { + data: id, + backdrop: false, + autoclose: true, + dragable: true, + }) + } + + private openCountryDetails(event: CountryEvent) { + // abort if we already have the country details open. + if (this.openedCountryDetails.find(cmp => cmp.countryCode === event.countryCode)) { + return; + } + + const ref = this.dialog.create(CountryDetailsComponent, { + data: { + name: event.countryName, + code: event.countryCode, + }, + autoclose: false, + dragable: true, + backdrop: false, + }) + const component = (ref.contentRef() as ComponentRef).instance; + + // used to track whether we highlighted a map pin + let hasPinHighlightActive = false; + + combineLatest([ + component.pinHover, + this.mapService.pins$, + ]) + .pipe( + takeUntil(ref.onClose), + ) + .subscribe(([hovered, pins]) => { + hasPinHighlightActive = hovered !== null; + + if (hovered !== null) { + this.onPinHover({ + mapPin: pins.find(p => p.pin.ID === hovered)!, + }) + this.highlightPin(hovered, true) + } else { + this.onPinHover(null); + this.clearPinHighlights(); + } + + + this.cdr.markForCheck(); + }) + + ref.onClose + .subscribe(() => { + if (hasPinHighlightActive) { + this.clearPinHighlights(); + } + + const index = this.openedCountryDetails.findIndex(cmp => cmp === component); + if (index >= 0) { + this.openedCountryDetails.splice(index, 1); + } + }) + + this.openedCountryDetails.push(component); + } + + private updateOverlayPositions() { + this.mapService.pinsMap$ + .pipe(take(1)) + .subscribe(allPins => { + this.selectedPins.forEach(pin => { + const pinObj = allPins.get(pin.mapPin.pin.ID); + if (!pinObj) { + return; + } + + pin.overlayRef.updatePosition(); + }) + }) + } + + onCountryClick(countryEvent: CountryEvent) { + this.openCountryDetails(countryEvent); + } + + onCountryHover(countryEvent: CountryEvent | null) { + if (this.countryDebounceTimer !== null) { + clearTimeout(this.countryDebounceTimer); + } + + if (!!countryEvent) { + this.hoveredCountry = { + countryCode: countryEvent.countryCode, + countryName: countryEvent.countryName, + } + this.cdr.markForCheck(); + + return; + } + + this.countryDebounceTimer = setTimeout(() => { + this.hoveredCountry = null; + this.countryDebounceTimer = null; + this.cdr.markForCheck(); + }, 200) + } + + onPinClick(pinEvent: PinEvent) { + // if the control key hold when clicking a map pin, we immediately open the + // pin details instead of the overlay. + if (pinEvent.event?.ctrlKey) { + this.openPinDetails(pinEvent.mapPin.pin.ID); + } + + const overlay = this.selectedPins.find(por => por.mapPin.pin.ID === pinEvent.mapPin.pin.ID); + if (!!overlay) { + overlay.disposeOverlay() + return; + } + + // if shiftKey was not pressed during the pinClick we dispose all active overlays that have not been + // moved by the user + if (!pinEvent.event?.shiftKey) { + this.selectedPins + .filter(overlay => !overlay.hasBeenMoved) + .forEach(selected => selected.disposeOverlay()) + } + + this.mapService.pinsMap$ + .pipe(take(1)) + .subscribe(async lm => { + const overlayComp = this.createPinOverlay(pinEvent, lm); + + // when the user wants to dispose a pin overlay (by clicking the X) we + // - make sure the pin is not highlighted anymore + // - remove the pin from the selectedPins list + // - remove lines showing the route to the home hub + overlayComp.afterDispose + .subscribe(pinID => { + this.highlightPin(pinID, false); + + const overlayIdx = this.selectedPins.findIndex(por => por.mapPin.pin.ID === pinEvent.mapPin.pin.ID); + this.selectedPins.splice(overlayIdx, 1) + + this.updatePaths() + this.cdr.markForCheck(); + }) + + // when the user hovers/leaves a pin overlay, we: + // - move the pin-overlay to the top when the user hovers it so stacking order is correct + // - (un)hightlight the pin element on the map + overlayComp.overlayHover + .subscribe(evt => { + this.highlightPin(evt.pinID, evt.type === 'enter') + + // over the overlay component to the top + if (evt.type === 'enter') { + this.selectedPins.forEach(ref => { + if (ref !== overlayComp && ref.overlayRef.hostElement) { + ref.overlayRef.hostElement.style.zIndex = '0'; + } + }) + + overlayComp.overlayRef.hostElement.style.zIndex = ''; + } + }) + + this.selectedPins.push(overlayComp) + + this.updatePaths([]); + this.cdr.markForCheck(); + }) + } + + private updatePaths(additional: Path[] = []) { + const paths = [ + ...(this.selectedPins + .reduce((list, pin) => { + if (pin.routeHome) { + list.push(pin.routeHome) + } + + return [ + ...list, + ...(pin.additionalPaths || []) + ] + }, [] as Path[])), + ...this.liveModePaths, + ...additional + ] + + this.paths = paths.map(p => { + return { + ...p, + attributes: { + class: 'lane', + ...(p.attributes || {}) + } + } + }); + + this.renderPaths(this.paths) + } + + onPinHover(pinEvent: PinEvent | null) { + if (!pinEvent) { + this.updatePaths([]); + this.onCountryHover(null); + + return; + } + + // we also emit a country hover event here to keep the country + // overlay open. + const countryName = this.mapRenderer.countryNames[pinEvent.mapPin.entity.Country] + this.onCountryHover({ + event: pinEvent.event, + countryCode: pinEvent.mapPin.entity.Country, + countryName: countryName!, + }) + + // in developer mode, we show all connected lanes of the hovered pin. + if (this.expertiseService.currentLevel === ExpertiseLevel.Developer) { + this.mapService.pinsMap$ + .pipe(take(1)) + .subscribe(lm => { + const lanes = this.getConnectedLanes(pinEvent?.mapPin, lm) + this.updatePaths(lanes); + this.cdr.markForCheck(); + }) + } + } + + /** + * Marks a process group as selected and either selects one or all exit pins + * of that group. If shiftKey is pressed during click, the ID(s) will be added + * to the list of selected pins instead of replacing it. If shiftKey is pressed + * the process group itself will NOT be displayed as selected. + * + * @private - template only + */ + selectGroup(grp: AppProfile, pin?: MapPin | null, event?: MouseEvent) { + if (!!pin) { + this.selectPins(event, of([pin.pin.ID])) + return; + } + + this.selectPins(event, this.mapService.getExitPinIDsForProfile(grp)) + } + + /** Returns a list of lines that represent the route from pin to home. */ + private getRouteHome(pin: MapPin, lm: Map, includeAllRoutes = false): Path[] { + let pinsToEval: MapPin[] = [pin]; + + // decide whether to draw all connection routes that travel through pin. + if (includeAllRoutes) { + pinsToEval = [ + ...pinsToEval, + ...Array.from(lm.values()) + .filter(p => p.pin.Route?.includes(pin.pin.ID)) + ] + } + + return pinsToEval.map(pin => ({ + id: `route-home-from-${pin.pin.ID}`, + points: (pin.pin.Route || []).map(hop => lm.get(hop)!), + attributes: { + 'in-use': 'true' + } + })); + } + + /** Returns a list of lines the represent all lanes to connected pins of pin */ + private getConnectedLanes(pin: MapPin, lm: Map): Path[] { + let result: Path[] = []; + + // add all lanes for connected hubs + Object.keys(pin.pin.ConnectedTo).forEach(target => { + const p = lm.get(target); + if (!!p) { + result.push({ + id: lineID([pin, p]), + points: [ + pin, + p + ] + }) + } + }); + + return result; + + } + + private async renderPaths(paths: Path[]) { + if (!this.mapRef) { + return; + } + + const ref = this.mapRef! + + const linesGroup: LaneGroup = this.mapRef.select("#line-group")! + + const self = this; + const renderedPaths = linesGroup.selectAll('path') + .data(paths, p => p.id); + + renderedPaths + .enter() + .append('path') + .attr('d', path => { + return self.lineFunc!(path.points) + }) + .attr("stroke-width", d => { + if (d.attributes) { + if (d.attributes['in-use']) { + return 2 / ref.zoomScale + } + } + + return 1 / ref.zoomScale; + }) + .call(sel => { + if (sel.empty()) { + return; + } + const data = sel.datum()?.attributes || {}; + Object.keys(data) + .forEach(key => { + sel.attr(key, data[key]) + }) + }) + .transition("enter-lane") + .duration(d => d.points.length * MapRendererComponent.LineAnimationDuration) + .attrTween('stroke-dasharray', tweenDashEnter) + + renderedPaths.exit() + .interrupt("enter-lane") + .transition("leave-lane") + .duration(200) + .attrTween('stroke-dasharray', tweenDashExit) + .remove(); + } + + private async renderPins(pins: MapPin[]) { + pins = pins.filter(pin => !pin.isOffline || pin.isActive); + + if (!this.mapRef) { + return + } + + const ref = this.mapRef!; + + const countriesWithNodes = new Set(); + + pins.forEach(pin => { + countriesWithNodes.add(pin.entity.Country) + }) + + const pinsGroup = ref.select('#pin-group')! + + const pinElements = pinsGroup + .selectAll('g') + .data(pins, pin => pin.pin.ID) + + const self = this; + + // add new pins + pinElements + .enter() + .append('g') + .append(d => { + const val = MapRendererComponent.MarkerSize / ref.zoomScale; + + if (d.isHome) { + const homeIcon = document.createElementNS('http://www.w3.org/2000/svg', 'circle') + homeIcon.setAttribute('r', `${val * 1.25}`) + + return homeIcon; + } + + if (d.pin.VerifiedOwner === 'Safing') { + const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon') + polygon.setAttribute('points', `0,-${val} -${val},${val} ${val},${val}`) + + return polygon; + } + + const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle') + circle.setAttribute('r', `${val}`) + + return circle; + }) + .attr("stroke-width", d => { + if (d.isExit || self.highlightedPins.has(d.pin.ID)) { + return 2 / ref.zoomScale + } + + if (d.isHome) { + return 4.5 / ref.zoomScale + } + + return 1 / ref.zoomScale + }) + .call(selection => { + selection + .style('opacity', 0) + .attr('transform', d => 'scale(0)') + .transition('enter-marker') + /**/.duration(1000) + /**/.attr('transform', d => `scale(1)`) + /**/.style('opacity', 1) + }) + .on('click', function (e: MouseEvent) { + const pin = select(this).datum() as MapPin; + self.onPinClick({ + event: e, + mapPin: pin + }); + }) + .on('mouseenter', function (e: MouseEvent) { + const pin = select(this).datum() as MapPin; + self.onPinHover({ + event: e, + mapPin: pin, + }) + }) + .on('mouseout', function (e: MouseEvent) { + self.onPinHover(null); + }) + + // remove pins from the map that disappeared + pinElements + .exit() + .remove() + + // update all pins to their correct position and update their attributes + pinsGroup.selectAll('g') + .attr('hub-id', d => d.pin.ID) + .attr('is-home', d => d.isHome) + .attr('transform', d => `translate(${ref.projection([d.location.Longitude, d.location.Latitude])})`) + .attr('in-use', d => d.isTransit) + .attr('is-exit', d => d.isExit) + .attr('raise', d => this.highlightedPins.has(d.pin.ID)) + + // update the attributes of the country shapes + ref.worldGroup.selectAll('path') + .attr('has-nodes', d => countriesWithNodes.has(d.properties.iso_a2)) + + // get all in-use pins and raise them to the top + pinsGroup.selectAll('g[in-use=true]') + .raise() + + // finally, re-raise all pins that are highlighted + pinsGroup.selectAll('g[raise=true]') + .raise() + + const activeCountrySet = new Set(); + pins.forEach(pin => { + if (pin.isTransit) { + activeCountrySet.add(pin.pin.ID) + } + }) + + // update the in-use attributes of the country shapes + ref.worldGroup.selectAll('path') + .attr('in-use', d => activeCountrySet.has(d.properties.iso_a2)) + + this.cdr.detectChanges(); + } + + public getPinElem(pinID: string) { + if (!this.mapRef) { + return + } + + return this.mapRef.root + .select("#pin-group") + .select(`g[hub-id=${pinID}]`) + .node() + } + + public clearPinHighlights() { + if (!this.mapRef) { + return + } + + this.mapRef.root + .select('#pin-group') + .select(`g[raise=true]`) + .attr('raise', false) + + this.highlightedPins.clear(); + } + + public highlightPin(pinID: string, highlight: boolean) { + if (highlight) { + this.highlightedPins.add(pinID) + } else { + this.highlightedPins.delete(pinID); + } + + if (!this.mapRef) { + return + } + const pinElemn = this.mapRef!.root + .select("#pin-group") + .select(`g[hub-id=${pinID}]`) + .attr('raise', highlight) + + if (highlight) { + pinElemn + .raise() + } + } +} + +function lineID(l: [MapPin, MapPin]): string { + return [l[0].pin.ID, l[1].pin.ID].sort().join("-") +} + +const tweenDashEnter = function (this: SVGPathElement) { + const len = this.getTotalLength(); + const interpolate = interpolateString(`0, ${len}`, `${len}, ${len}`); + return (t: number) => { + if (t === 1) { + return '0'; + } + return interpolate(t); + } +} + +const tweenDashExit = function (this: SVGPathElement) { + const len = this.getTotalLength(); + const interpolate = interpolateString(`${len}, ${len}`, `0, ${len}`); + return (t: number) => { + if (t === 1) { + return `${len}`; + } + return interpolate(t); + } +} diff --git a/desktop/angular/src/app/pages/spn/spn.module.ts b/desktop/angular/src/app/pages/spn/spn.module.ts new file mode 100644 index 00000000..737ae25f --- /dev/null +++ b/desktop/angular/src/app/pages/spn/spn.module.ts @@ -0,0 +1,69 @@ +import { A11yModule } from '@angular/cdk/a11y'; +import { DragDropModule } from '@angular/cdk/drag-drop'; +import { OverlayModule } from '@angular/cdk/overlay'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { SfngToggleSwitchModule, SfngTooltipModule, TabModule } from '@safing/ui'; +import { SfngAppIconModule } from 'src/app/shared/app-icon'; +import { CountIndicatorModule } from 'src/app/shared/count-indicator'; +import { CountryFlagModule } from 'src/app/shared/country-flag'; +import { ExpertiseModule } from 'src/app/shared/expertise/expertise.module'; +import { SfngFocusModule } from 'src/app/shared/focus'; +import { SfngMenuModule } from 'src/app/shared/menu'; +import { CommonPipesModule } from 'src/app/shared/pipes'; +import { SpnPageComponent } from './'; +import { CountryDetailsComponent } from './country-details'; +import { CountryOverlayComponent } from './country-overlay'; +import { SpnMapLegendComponent } from './map-legend'; +import { MapRendererComponent } from './map-renderer'; +import { SpnNodeIconComponent } from './node-icon'; +import { PinDetailsComponent } from './pin-details'; +import { SpnPinListComponent } from './pin-list/pin-list'; +import { PinOverlayComponent } from './pin-overlay'; +import { SpnPinRouteComponent } from './pin-route'; +import { SPNFeatureCarouselComponent } from './spn-feature-carousel'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + CountryFlagModule, + SfngTooltipModule, + SfngMenuModule, + SfngFocusModule, + SfngAppIconModule, + SfngToggleSwitchModule, + TabModule, + A11yModule, + ExpertiseModule, + OverlayModule, + CountIndicatorModule, + FontAwesomeModule, + CommonPipesModule, + DragDropModule, + RouterModule, + ], + declarations: [ + MapRendererComponent, + PinOverlayComponent, + CountryOverlayComponent, + CountryDetailsComponent, + SpnNodeIconComponent, + SpnMapLegendComponent, + PinDetailsComponent, + SpnPinRouteComponent, + SPNFeatureCarouselComponent, + SpnPageComponent, + SpnPinListComponent, + ], + exports: [ + SpnPageComponent, + SpnPinRouteComponent, + SpnNodeIconComponent, + MapRendererComponent, + ] +}) +export class SPNModule { } diff --git a/desktop/angular/src/app/pages/spn/utils.ts b/desktop/angular/src/app/pages/spn/utils.ts new file mode 100644 index 00000000..eaeefe49 --- /dev/null +++ b/desktop/angular/src/app/pages/spn/utils.ts @@ -0,0 +1,4 @@ +import { OverlayRef } from '@angular/cdk/overlay'; +import { InjectionToken } from '@angular/core'; + +export const OVERLAY_REF = new InjectionToken('OVERLAY_REF'); diff --git a/desktop/angular/src/app/pages/support/form/index.ts b/desktop/angular/src/app/pages/support/form/index.ts new file mode 100644 index 00000000..b28d7e24 --- /dev/null +++ b/desktop/angular/src/app/pages/support/form/index.ts @@ -0,0 +1 @@ +export * from './support-form'; diff --git a/desktop/angular/src/app/pages/support/form/support-form.html b/desktop/angular/src/app/pages/support/form/support-form.html new file mode 100644 index 00000000..10685d99 --- /dev/null +++ b/desktop/angular/src/app/pages/support/form/support-form.html @@ -0,0 +1,107 @@ +
+ + +
+ +
+
+
+

{{ page?.title }}

+
+ +

+ {{ page?.prologue || page?.shortHelp }} +

+ +
+

{{ page?.repoHelp }}

+ +
+ +

Title

+
+ + Copy +
+ +
+

{{section.title}}

+
+ + Copy +
+ +
+ + +
+

Included Debug Info

+
+ +

+ The following debug information will be sent together with your report. Please check it and remove potentially sensitive + information. The debug information sent with your reports will be saved on Safing's self-hosted pastebin server + and is viewable via its created url. The data is automatically destroyed after one month. +

+
+
+ Portmaster Version: {{version}} + built on {{buildDate}} +
+ Copy +
+ +
+ +
+ + +
+
+ +
+
+

+ Related Issues + +

+
+

+ Public issues related to your title: +

+ +

+ No related issues were found. +

+ +
    +
  • + {{ issue.title }} + {{ issue.closed ? 'closed' : 'opened'}} in {{ repos[issue.repository] || issue.repository + }} by {{ issue.user }} + {{ + issue.createdAt | timeAgo + }} + +
  • +
+
+
diff --git a/desktop/angular/src/app/pages/support/form/support-form.scss b/desktop/angular/src/app/pages/support/form/support-form.scss new file mode 100644 index 00000000..7555aa08 --- /dev/null +++ b/desktop/angular/src/app/pages/support/form/support-form.scss @@ -0,0 +1,253 @@ +:host { + width: 100%; + display: flex; + flex-grow: 1; + flex-direction: column; + height: 100%; +} + +.scroll-container { + overflow: auto; + margin-right: 1rem; + display: flex; + flex-direction: row; + justify-content: center; + flex-grow: 1; + + @apply p-8; + + h3 { + opacity: .9; + font-size: 0.95rem; + } +} + +.form-wrapper { + flex-grow: 2; + + @media (min-width: 1250px) { + max-width: 800px; + } + +} + +.issue-list { + width: 400px; + + margin-left: 2rem; + + &, + ul { + overflow-y: hidden; + } + + .issue { + @apply px-4; + @apply pr-8; + @apply py-4; + @apply rounded; + @apply bg-cards-secondary; + + span { + word-break: keep-all; + } + + display : flex; + flex-direction: column; + position : relative; + cursor : pointer; + + &:not(:last-child) { + margin-bottom: 0.5rem; + } + + .meta { + @apply text-tertiary; + @apply font-normal; + opacity: .7; + font-size: 95%; + } + + &:hover { + @apply bg-cards-tertiary; + } + + fa-icon { + position: absolute; + right: calc((2rem - 12px) / 2); + top: calc(50% - 8px); // actually the half height is 6px but that looks off for the icon we're using + opacity: .3; + } + } +} + +p.prologue { + @apply mb-8; +} + +.page-title { + margin-top: 20px; + margin-bottom: 40px; + position: relative; + border-bottom: 1px solid rgba(255, 255, 255, .2); + + h1 { + position: absolute; + top: -1rem; + background-color: var(--background); + @apply pr-8; + } +} + +.repo-list { + @apply mb-8; + +} + +button { + @apply p-2; + @apply bg-buttons-dark; + @apply border; + @apply border-buttons-dark; + opacity: .4; + + &:not(:last-child) { + @apply mr-1; + } + + &:hover { + @apply bg-buttons-light; + @apply border-buttons-light; + } + + &.selected { + @apply bg-buttons-dark; + @apply border-buttons-light; + opacity: 1; + } +} + +.actions { + @apply mt-8; + @apply pb-16; + + button { + opacity: 1; + @apply bg-transparent; + + &.primary { + @apply bg-buttons-dark; + opacity: 1; + } + + &:hover { + @apply bg-buttons-light; + } + } + +} + +.debug-header { + height: 32px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + position: relative; + @apply bg-cards-primary; + @apply rounded-t; + top: 2px; +} + +textarea { + @apply px-4; + @apply py-2; + min-height: 40px; +} + +textarea, +input[type="text"].title { + @apply font-medium; + @apply border; + @apply border-cards-secondary; + @apply bg-cards-secondary; + padding-right: 4.5rem; // copy button width + + &:hover, + &:active, + &:focus { + @apply border-cards-primary; + } +} + +input[type="text"].title { + padding-left: 1rem; +} + +section { + @apply py-8; + + &:not(:first-of-type) { + @apply pt-0; + } +} + +.input-wrapper { + position: relative; + display: flex; +} + +.copy-button { + user-select: none; + position: absolute; + top: 1px; + right: 0px; + width: 4rem; + height: 31px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + cursor: pointer; + @apply bg-buttons-dark; + @apply rounded-sm; + opacity: .5; + + &:hover { + opacity: .9; + } +} + +.section-help { + @apply bg-cards-primary; + @apply border-t; + @apply border-dashed; + @apply border-buttons-light; + @apply p-2; + @apply px-4; + @apply rounded-sm; + color: rgba(255, 255, 255, .6); + font-size: 0.7rem; + position: relative; + width: 100%; + display: flex; + flex-direction: column; +} + +.gh-author { + @apply mt-8; + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-start; + + .input-wrapper { + padding-top: 2px; + @apply pr-2; + } +} + +input[type="text"].missing, +textarea.missing { + @apply border-info-red; +} diff --git a/desktop/angular/src/app/pages/support/form/support-form.ts b/desktop/angular/src/app/pages/support/form/support-form.ts new file mode 100644 index 00000000..1b2e8ed1 --- /dev/null +++ b/desktop/angular/src/app/pages/support/form/support-form.ts @@ -0,0 +1,258 @@ +import { CdkScrollable } from '@angular/cdk/scrolling'; +import { Component, DestroyRef, OnInit, TrackByFunction, ViewChild, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ActivatedRoute, Router } from '@angular/router'; +import { DebugAPI } from '@safing/portmaster-api'; +import { ConfirmDialogConfig, SfngDialogService } from '@safing/ui'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { debounceTime, mergeMap } from 'rxjs/operators'; +import { SessionDataService, StatusService } from 'src/app/services'; +import { Issue, SupportHubService } from 'src/app/services/supporthub.service'; +import { ActionIndicatorService } from 'src/app/shared/action-indicator'; +import { fadeInAnimation, fadeInListAnimation, moveInOutAnimation } from 'src/app/shared/animations'; +import { FuzzySearchService } from 'src/app/shared/fuzzySearch'; +import { SupportPage, supportTypes } from '../pages'; +import { INTEGRATION_SERVICE } from 'src/app/integration'; +import { SupportProgressDialogComponent, TicketData, TicketInfo } from '../progress-dialog'; + +@Component({ + templateUrl: './support-form.html', + styleUrls: ['./support-form.scss'], + animations: [fadeInAnimation, moveInOutAnimation, fadeInListAnimation] +}) +export class SupportFormComponent implements OnInit { + private readonly destroyRef = inject(DestroyRef); + private readonly search$ = new BehaviorSubject(''); + private readonly integration = inject(INTEGRATION_SERVICE); + + page: SupportPage | null = null; + + debugData: string = ''; + title: string = ''; + form: { [key: string]: string } = {} + selectedRepo: string = ''; + haveGhAccount = false; + version: string = ''; + buildDate: string = ''; + titleMissing = false; + + relatedIssues: Issue[] = []; + allIssues: Issue[] = []; + repos: { [repo: string]: string } = {}; + + @ViewChild(CdkScrollable) + scrollContainer: CdkScrollable | null = null; + + trackIssue: TrackByFunction = (_: number, issue: Issue) => issue.url; + + constructor( + private route: ActivatedRoute, + private router: Router, + private uai: ActionIndicatorService, + private debugapi: DebugAPI, + private statusService: StatusService, + private dialog: SfngDialogService, + private supporthub: SupportHubService, + private searchService: FuzzySearchService, + private sessionService: SessionDataService, + ) { } + + ngOnInit() { + this.supporthub.loadIssues().subscribe(issues => { + issues = issues.reverse(); + this.allIssues = issues; + this.relatedIssues = issues; + }) + + this.search$.pipe( + takeUntilDestroyed(this.destroyRef), + debounceTime(200), + ) + .subscribe((text) => { + this.relatedIssues = this.searchService.searchList(this.allIssues, text, { + disableHighlight: true, + shouldSort: true, + isCaseSensitive: false, + minMatchCharLength: 4, + keys: [ + 'title', + 'body', + ], + }).map(res => res.item) + }) + + this.statusService.getVersions() + .subscribe(status => { + this.version = status.Core.Version; + this.buildDate = status.Core.BuildDate; + }) + + this.route.paramMap + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(params => { + const id = params.get("id") + for (let pIdx = 0; pIdx < supportTypes.length; pIdx++) { + const pageSection = supportTypes[pIdx]; + const page = pageSection.choices.find(choice => choice.type !== 'link' && choice.id === id); + if (!!page) { + this.page = page as SupportPage; + break; + } + } + + if (!this.page) { + this.router.navigate(['..']); + return; + } + this.title = ''; + this.form = {}; + this.selectedRepo = 'portmaster'; + this.debugData = ''; + this.repos = {}; + this.page.sections.forEach(section => this.form[section.title] = ''); + this.page.repositories?.forEach(repo => this.repos[repo.repo] = repo.name) + + // try to restore from session service + this.sessionService.restore(this.page.id, this); + + if (this.page.includeDebugData) { + this.debugapi.getCoreDebugInfo('github') + .subscribe({ + next: data => this.debugData = data, + error: err => this.uai.error('Failed to get Debug Data', this.uai.getErrorMessgae(err)) + }) + } + }) + } + + onModelChange() { + if (!this.page) { + return; + } + this.sessionService.save(this.page.id, this, ['title', 'form', 'selectedRepo', 'haveGhAccount']); + } + + selectRepo(repo: string) { + this.selectedRepo = repo; + this.onModelChange(); + } + + searchIssues(text: string) { + this.onModelChange(); + this.search$.next(text); + } + + copyToClipboard(what: string) { + this.integration.writeToClipboard(what) + .then(() => this.uai.success("Copied to Clipboard")) + .catch(() => this.uai.error('Failed to Copy to Clipboard')); + } + + validate(): boolean { + this.titleMissing = this.title === ''; + const valid = !this.titleMissing; + if (!valid) { + this.scrollContainer?.scrollTo({ top: 0, behavior: 'smooth' }) + } + return valid; + } + + createIssue(type: 'github' | 'private', genUrl?: boolean, email?: string) { + const ticketData: TicketData = { + repo: this.selectedRepo || '', + title: this.title, + debugInfo: this.debugData, + sections: this.page?.sections.map(section => ({ + title: section.title, + body: this.form[section.title], + })) || [], + } + + let issue: TicketInfo; + + switch (type) { + case 'github': + issue = { + type: 'github', + generateUrl: genUrl || false, + preset: this.page!.ghIssuePreset || '', + ...ticketData + }; + + break; + + case 'private': + issue = { + type: 'private', + email: email, + ...ticketData + } + + break; + } + + SupportProgressDialogComponent.open(this.dialog, issue) + .subscribe(() => { + this.sessionService.delete(this.page?.id || ''); + }); + } + + createOnGithub(genUrl?: boolean) { + if (!this.validate()) { + return; + } + + if (genUrl === undefined && this.haveGhAccount) { + genUrl = true; + } + + if (genUrl === undefined) { + this.dialog.confirm({ + canCancel: true, + caption: 'Caution', + header: 'Create Issue on GitHub', + message: 'You can easily create the issue with your own GitHub account. Or create the GitHub issue privately, but then we will have no way to communicate with you for further information.', + buttons: [ + { id: 'createWithout', text: 'Create Without Account', class: 'outline' }, + { id: 'openGithub', text: 'Use My Account' }, + ] + }) + .onAction('openGithub', () => { + this.createIssue('github', true) + }) + .onAction('createWithout', () => { + this.createIssue('github', false) + }) + return; + } + } + + openIssue(issue: Issue) { + this.integration.openExternal(issue.url); + } + + createPrivateTicket() { + if (!this.validate()) { + return; + } + + const opts: ConfirmDialogConfig = { + caption: 'Info', + canCancel: true, + header: 'How should we stay in touch?', + message: 'Please enter your email address so we can write back and forth until the issue is concluded.', + inputModel: '', + inputPlaceholder: 'Optional Email', + inputType: 'text', + buttons: [ + { id: '', class: 'outline', text: 'Cancel' }, + { id: 'create', text: 'Create Ticket' }, + ], + } + this.dialog.confirm(opts) + .onAction('create', () => { + this.createIssue('private', undefined, opts.inputModel); + }); + } + +} diff --git a/desktop/angular/src/app/pages/support/index.ts b/desktop/angular/src/app/pages/support/index.ts new file mode 100644 index 00000000..5f9360ad --- /dev/null +++ b/desktop/angular/src/app/pages/support/index.ts @@ -0,0 +1 @@ +export * from './support'; diff --git a/desktop/angular/src/app/pages/support/pages.ts b/desktop/angular/src/app/pages/support/pages.ts new file mode 100644 index 00000000..83cdcc67 --- /dev/null +++ b/desktop/angular/src/app/pages/support/pages.ts @@ -0,0 +1,175 @@ +export interface PageSections { + title?: string; + choices: SupportType[]; + style?: 'small'; +} + +export interface QuestionSection { + title: string; + help?: string; +} + +export interface SupportPage { + type?: undefined; + id: string; + title: string; + shortHelp: string; + repoHelp?: string; + prologue?: string; + epilogue?: string; + sections: QuestionSection[]; + privateTicket?: boolean; + ghIssuePreset?: string; + includeDebugData?: boolean; + repositories?: { repo: string, name: string }[]; +} + +export interface ExternalLink { + type: 'link', + url: string; + title: string; + shortHelp: string; +} + +export type SupportType = SupportPage | ExternalLink; + +export const supportTypes: PageSections[] = [ + { + title: "Resources", + choices: [ + { + type: 'link', + title: '📘 Portmaster Wiki & FAQ', + url: 'https://wiki.safing.io/?source=Portmaster', + shortHelp: 'Search the Portmaster knowledge base and FAQ.', + }, + { + type: 'link', + title: '🔖 Settings Handbook', + url: 'https://docs.safing.io/portmaster/settings?source=Portmaster', + shortHelp: 'A reference document of all Portmaster settings.' + }, + { + type: 'link', + title: '📑 Safing Blog', + url: 'https://safing.io/blog?source=Portmaster', + shortHelp: 'Read our blog posts and announcements.', + } + ] + }, + { + title: "Communities & Support", + style: 'small', + choices: [ + { + type: 'link', + title: 'Join us on Discord', + url: 'https://discord.gg/safing', + shortHelp: 'Get help from the community and our AI bot on Discord.' + }, + { + type: 'link', + title: 'Follow us on Mastodon', + url: 'https://fosstodon.org/@safing', + shortHelp: 'Get updates and privacy jokes on Mastodon.' + }, + { + type: 'link', + title: 'Follow us on Twitter', + url: 'https://twitter.com/SafingIO', + shortHelp: 'Get updates and privacy jokes on Twitter.' + }, + { + type: 'link', + title: 'Safing Support via Email', + url: 'mailto:support@safing.io', + shortHelp: 'As a subscriber, reach out to the Safing team directly.' + } + ] + }, + { + title: "Make a Report", + style: 'small', + choices: [ + { + id: "report-bug", + title: "🐞 Report a Bug", + shortHelp: "Found a bug? Report your discovery and make the Portmaster better for everyone.", + repoHelp: "Where did the bug take place?", + sections: [ + { + title: "What happened?", + help: "Describe what happened in detail" + }, + { + title: "What did you expect to happen?", + help: "Describe what you expected to happen instead" + }, + { + title: "How did you reproduce it?", + help: "Describe how to reproduce the issue" + }, + { + title: "Additional information", + help: "Provide extra details if needed" + }, + ], + includeDebugData: true, + privateTicket: true, + ghIssuePreset: "report-bug.md", + repositories: [ + { repo: 'portmaster', name: 'Portmaster Core' }, + { repo: 'portmaster-ui', name: 'User Interface' }, + { repo: 'portmaster-packaging', name: 'Packaging & Installers' }, + { repo: 'spn', name: 'SPN' }, + ] + }, + { + id: "give-feedback", + title: "💡 Suggest an Improvement", + shortHelp: "Suggest an enhancement or a new feature for Portmaster.", + repoHelp: "What would you would like to improve?", + sections: [ + { + title: "What would you like to add or change?", + }, + { + title: "Why do you and others need this?" + } + ], + includeDebugData: false, + privateTicket: true, + ghIssuePreset: "suggest-feature.md", + repositories: [ + { repo: 'portmaster', name: 'Portmaster Core' }, + { repo: 'portmaster-ui', name: 'User Interface' }, + { repo: 'portmaster-packaging', name: 'Packaging & Installers' }, + { repo: 'spn', name: 'SPN' }, + ] + }, + { + id: "compatibility-report", + title: "📝 Make a Compatibility Report", + shortHelp: "Report Portmaster in/compatibility with Linux Distros, VPN Clients or general Software.", + sections: [ + { + title: "What worked?", + help: "Describe what worked" + }, + { + title: "What did not work?", + help: "Describe what did not work in detail" + }, + { + title: "Additional information", + help: "Provide extra details if needed" + }, + ], + includeDebugData: true, + privateTicket: true, + ghIssuePreset: "report-compatibility.md", + repositories: [] // not needed with the default being "portmaster" + }, + ], + } +] diff --git a/desktop/angular/src/app/pages/support/progress-dialog/index.ts b/desktop/angular/src/app/pages/support/progress-dialog/index.ts new file mode 100644 index 00000000..0dfbf366 --- /dev/null +++ b/desktop/angular/src/app/pages/support/progress-dialog/index.ts @@ -0,0 +1 @@ +export * from './progress-dialog'; diff --git a/desktop/angular/src/app/pages/support/progress-dialog/progress-dialog.html b/desktop/angular/src/app/pages/support/progress-dialog/progress-dialog.html new file mode 100644 index 00000000..2504a56e --- /dev/null +++ b/desktop/angular/src/app/pages/support/progress-dialog/progress-dialog.html @@ -0,0 +1,114 @@ + +
+ + Status + +
+ + + + +
+ + + Uploading debug data .... + + + + + Creating GitHub issue ... + + + + + Creating private support ticket ... + +
+
+
+ + + + + + + + + + + + Ticket prepared successfully + + + + Ticket created successfully! + + + + +
+ Use the following button to open the pre-filled GitHub issue form: + +
+ +
+ +
+
+ +
+ + We successfully create the issue on GitHub for you. +
+ Use the following link to check for updates: +
+ + {{ url }} +
+ + + We will contact you as soon as possbile. + +
+ + +
+ + + + + + + + Failed to create Support Ticket + + + + + + An error occured while creating your support ticket: + + + + {{ error || 'Unknown Error' }} + +
+ + +
+ + + + + + +
diff --git a/desktop/angular/src/app/pages/support/progress-dialog/progress-dialog.ts b/desktop/angular/src/app/pages/support/progress-dialog/progress-dialog.ts new file mode 100644 index 00000000..32deb205 --- /dev/null +++ b/desktop/angular/src/app/pages/support/progress-dialog/progress-dialog.ts @@ -0,0 +1,173 @@ +import { ComponentPortal } from "@angular/cdk/portal"; +import { HttpErrorResponse } from "@angular/common/http"; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentRef, EventEmitter, OnInit, inject } from "@angular/core"; +import { SFNG_DIALOG_REF, SfngDialogRef, SfngDialogService } from "@safing/ui"; +import { Observable, map, mergeMap, of } from "rxjs"; +import { INTEGRATION_SERVICE } from "src/app/integration"; +import { SupportHubService, SupportSection } from "src/app/services"; +import { ActionIndicatorService } from "src/app/shared/action-indicator"; + +export interface TicketData { + debugInfo: string; + repo: string; + title: string; + sections: SupportSection[]; +} + +export interface GithubIssue extends TicketData { + type: 'github', + generateUrl?: boolean; + preset?: string; +} + +export interface PrivateTicket extends TicketData { + type: 'private', + email?: string, +} + +export type TicketInfo = GithubIssue | PrivateTicket; + + +@Component({ + templateUrl: './progress-dialog.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styles: [ + ` + :host { + @apply block flex flex-col gap-8 relative; + } + `, + ] +}) +export class SupportProgressDialogComponent implements OnInit { + + /** Static method to open the support-progress dialog. */ + static open(dialog: SfngDialogService, data: TicketInfo): Observable { + const ref = dialog.create(SupportProgressDialogComponent, { + data, + dragable: true, + backdrop: false, + autoclose: false, + }); + + return (ref.contentRef() as ComponentRef) + .instance + .done; + } + + + private readonly cdr = inject(ChangeDetectorRef); + private readonly supporthub = inject(SupportHubService); + private readonly uai = inject(ActionIndicatorService); + + readonly integration = inject(INTEGRATION_SERVICE); + + readonly dialogRef: SfngDialogRef = inject(SFNG_DIALOG_REF); + + /** Holds the current state of the issue-creation */ + state: '' | 'debug-info' | 'create-issue' | 'create-ticket' | 'done' | 'error' = ''; + + /** The URL to the github issue once it was created. */ + url: string = ''; + + /** The error message if one occured */ + error: string = ''; + + /** Emits once the issue has been created successfully */ + done = new EventEmitter; + + ngOnInit(): void { + this.createSupportRequest(); + } + + setState(state: typeof this['state']) { + this.state = state; + this.cdr.detectChanges(); + } + + createSupportRequest(): void { + const data = this.dialogRef.data; + let stream = of('') + + // Upload debug info + if (data.debugInfo) { + stream = new Observable((observer) => { + this.state = 'debug-info'; + this.cdr.detectChanges(); + + this.supporthub.uploadText('debug-info', data.debugInfo) + .subscribe(observer); + }) + } + + // either create on github or create a private ticket through support-hub + if (data.type === 'github') { + stream = stream.pipe( + mergeMap((url) => { + this.state = 'create-issue'; + this.cdr.detectChanges(); + + return this.supporthub.createIssue( + data.repo, + data.preset || '', + data.title, + data.sections, + url, + { + generateUrl: data.generateUrl || false + }, + ); + }) + ) + } else { + stream = stream.pipe( + mergeMap((url) => { + this.state = 'create-ticket'; + this.cdr.markForCheck(); + + return this.supporthub.createTicket( + data.repo, + data.title, + data.email || '', + data.sections, + url + ) + }), + map(() => '') + ) + } + + stream.subscribe({ + next: (url) => { + this.state = 'done'; + this.url = url; + this.cdr.markForCheck(); + + this.done.next(); + }, + + error: (err) => { + console.error("error", err); + + this.state = 'error'; + if (err instanceof HttpErrorResponse && err.error instanceof ProgressEvent) { + this.error = err.statusText; + } else { + this.error = this.uai.getErrorMessage(err); + } + + this.cdr.markForCheck(); + } + }); + } + + copyUrl() { + if (!this.url) { + return + } + + this.integration.writeToClipboard(this.url) + .then(() => this.uai.success('URL Copied To Clipboard')) + .catch(err => this.uai.error('Failed to Copy To Clipboard', this.uai.getErrorMessage(err))) + } +} diff --git a/desktop/angular/src/app/pages/support/support.html b/desktop/angular/src/app/pages/support/support.html new file mode 100644 index 00000000..2ad9eca2 --- /dev/null +++ b/desktop/angular/src/app/pages/support/support.html @@ -0,0 +1,50 @@ +
+ + +
+ +
+
+
+

{{section.title}}

+
+ +
+
+ + +

{{item.title}}

+ + +
+
+
+
+ + + +
diff --git a/desktop/angular/src/app/pages/support/support.scss b/desktop/angular/src/app/pages/support/support.scss new file mode 100644 index 00000000..fd3ddd50 --- /dev/null +++ b/desktop/angular/src/app/pages/support/support.scss @@ -0,0 +1,77 @@ +:host { + width: 100%; + display: flex; + flex-direction: column; + height: 100%; + overflow: auto; +} + +.list-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + + .section-title { + margin-top: 20px; + margin-bottom: 40px; + position: relative; + border-bottom: 1px solid rgba(255, 255, 255, .2); + + h4 { + position: absolute; + top: -0.5rem; + background-color: var(--background); + @apply pr-8; + } + } + + .page-section { + width: 100%; + display: flex; + justify-content: stretch; + align-items: stretch; + flex-direction: column; + @apply px-4; + + @media (min-width: 1250px) { + max-width: 800px; + } + } + + .option-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-gap: 20px; + grid-auto-rows: 1fr; + + width: 100%; + margin-bottom: 20px; + + section { + @apply bg-cards-secondary; + @apply p-8; + @apply rounded; + transition: all 250ms ease-in-out; + position: relative; + cursor: pointer; + + &:hover { + @apply bg-cards-tertiary; + } + + fa-icon { + position: absolute; + top: 1rem; + right: 1rem; + opacity: .4; + } + } + + + } + + .small .option-list section { + @apply p-4; + } +} diff --git a/desktop/angular/src/app/pages/support/support.ts b/desktop/angular/src/app/pages/support/support.ts new file mode 100644 index 00000000..4bd89940 --- /dev/null +++ b/desktop/angular/src/app/pages/support/support.ts @@ -0,0 +1,97 @@ +import { Component, DestroyRef, OnInit, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Router } from '@angular/router'; +import { BehaviorSubject, combineLatest, debounceTime } from 'rxjs'; +import { Issue, SupportHubService } from 'src/app/services'; +import { fadeInAnimation, fadeInListAnimation } from 'src/app/shared/animations'; +import { FuzzySearchService } from 'src/app/shared/fuzzySearch'; +import { SupportType, supportTypes } from './pages'; +import { INTEGRATION_SERVICE } from 'src/app/integration'; + +@Component({ + templateUrl: './support.html', + styleUrls: ['./support.scss'], + animations: [ + fadeInListAnimation, + fadeInAnimation, + ] +}) +export class SupportPageComponent implements OnInit { + // make supportTypes available in the page template. + readonly supportTypes = supportTypes; + + private readonly destroyRef = inject(DestroyRef); + private readonly integration = inject(INTEGRATION_SERVICE); + + /** @private The current search term for the FAQ entries. */ + searchFaqs = new BehaviorSubject(''); + + searchTerm: string = ''; + + /** A list of all faq entries loaded from the Support Hub */ + allFaqEntries: Issue[] = []; + + /** A list of faq entries to show */ + faqEntries: Issue[] = []; + + constructor( + private router: Router, + private searchService: FuzzySearchService, + private supportHub: SupportHubService, + ) { } + + ngOnInit(): void { + combineLatest([ + this.searchFaqs, + this.supportHub.loadIssues() + ]) + .pipe( + takeUntilDestroyed(this.destroyRef), + debounceTime(200), + ) + .subscribe(([searchTerm, allFaqEntries]) => { + this.allFaqEntries = allFaqEntries + .filter(issue => issue.labels?.includes("faq")) + .map(issue => { + return { + ...issue, + + title: issue.title.replace("FAQ: ", "") + } + }) + + if (searchTerm === '') { + this.faqEntries = [ + ...this.allFaqEntries + ] + + return; + } + + this.faqEntries = this.searchService.searchList(this.allFaqEntries, searchTerm, { + disableHighlight: true, + shouldSort: true, + isCaseSensitive: false, + minMatchCharLength: 3, + keys: [ + 'title', + 'body', + ], + }).map(res => res.item) + }) + } + + openIssue(issue: Issue) { + this.integration.openExternal(issue.url); + } + + openPage(item: SupportType) { + if (item.type === 'link') { + this.integration.openExternal(item.url); + return; + } + + this.router.navigate(['/support', item.id]); + } +} + diff --git a/desktop/angular/src/app/prompt-entrypoint/prompt-entrypoint.ts b/desktop/angular/src/app/prompt-entrypoint/prompt-entrypoint.ts new file mode 100644 index 00000000..23bf9f01 --- /dev/null +++ b/desktop/angular/src/app/prompt-entrypoint/prompt-entrypoint.ts @@ -0,0 +1,78 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit, TrackByFunction, inject } from "@angular/core"; +import { AppProfile, AppProfileService, PortapiService } from "@safing/portmaster-api"; +import { combineLatest, combineLatestAll, forkJoin, map, merge, mergeAll, of, switchMap } from "rxjs"; +import { ConnectionPrompt, NotificationType, NotificationsService } from "../services"; +import { SfngAppIconModule } from "../shared/app-icon"; +import { getCurrent } from '@tauri-apps/api/window'; +import { CountryFlagModule } from "../shared/country-flag"; + +interface Prompt { + prompts: ConnectionPrompt[]; + profile: AppProfile; +} + +@Component({ + standalone: true, + selector: 'app-root', + templateUrl: './prompt.html', + imports: [ + CommonModule, + SfngAppIconModule, + CountryFlagModule + ] +}) +export class PromptEntryPointComponent implements OnInit { + private readonly notificationService = inject(NotificationsService); + private readonly portapi = inject(PortapiService); + private readonly profileService = inject(AppProfileService); + + prompts: Prompt[] = []; + + trackPrompt: TrackByFunction = (_, p) => p.EventID; + trackProfile: TrackByFunction = (_, p) => p.profile._meta!.Key; + + ngOnInit(): void { + + this.notificationService + .new$ + .pipe( + map(notifs => { + return notifs.filter(n => n.Type === NotificationType.Prompt && n.EventID.startsWith("filter:prompt")) + }), + switchMap(notifications => { + const distictProfiles = new Map(); + notifications.forEach(n => { + const key = `${n.EventData!.Profile.Source}/${n.EventData!.Profile.ID}` + const arr = distictProfiles.get(key) || []; + arr.push(n); + distictProfiles.set(key, arr); + }); + + if (distictProfiles.size === 0) { + return of([]); + } + + return combineLatest(Array.from(distictProfiles.entries()).map(([key, prompts]) => forkJoin({ + profile: this.profileService.getAppProfile(key), + prompts: of(Array.from(prompts)) + }))); + }) + ) + .subscribe(result => { + this.prompts = result; + + // show the prompt now since we're ready + if (this.prompts.length) { + getCurrent()!.show(); + } + }) + } + + selectAction(prompt: ConnectionPrompt, action: string) { + prompt.SelectedActionID = action; + + this.portapi.update(prompt._meta!.Key, prompt) + .subscribe(); + } +} diff --git a/desktop/angular/src/app/prompt-entrypoint/prompt.html b/desktop/angular/src/app/prompt-entrypoint/prompt.html new file mode 100644 index 00000000..8667398e --- /dev/null +++ b/desktop/angular/src/app/prompt-entrypoint/prompt.html @@ -0,0 +1,65 @@ +
+ +
+ +

Portmaster

+
+ +
+ + + + +
+
+ + + {{ prompt.profile.Name }} + {{ prompt.profile.LinkedPath }} + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
Domain: + + + {{ prompt.EventData?.Entity?.Domain || 'N/A' }} + + +
+ +
+
IP:{{ prompt.EventData?.Entity?.IP || 'N/A' }}
+
+
+
+
+
+
+ +
\ No newline at end of file diff --git a/desktop/angular/src/app/services/index.ts b/desktop/angular/src/app/services/index.ts new file mode 100644 index 00000000..d4b95f1d --- /dev/null +++ b/desktop/angular/src/app/services/index.ts @@ -0,0 +1,8 @@ +export { NotificationsService } from './notifications.service'; +export * from './notifications.types'; +export * from './session-data.service'; +export { StatusService } from './status.service'; +export * from './status.types'; +export * from './supporthub.service'; +export * from './ui-state.service'; + diff --git a/desktop/angular/src/app/services/notifications.service.spec.ts b/desktop/angular/src/app/services/notifications.service.spec.ts new file mode 100644 index 00000000..8789bf32 --- /dev/null +++ b/desktop/angular/src/app/services/notifications.service.spec.ts @@ -0,0 +1,354 @@ +import { TestBed } from '@angular/core/testing'; +import { WebsocketService } from '@safing/portmaster-api'; +import { MockWebSocketSubject } from '@safing/portmaster-api/testing'; +import { PartialObserver } from 'rxjs'; +import { NotificationsService } from './notifications.service'; +import { Notification, NotificationType } from './notifications.types'; + +describe('NotificationsService', () => { + let service: NotificationsService; + let mock: MockWebSocketSubject; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { + provide: WebsocketService, + useValue: MockWebSocketSubject, + } + ] + }); + service = TestBed.inject(NotificationsService); + mock = MockWebSocketSubject.lastMock!; + }); + + afterEach(() => { + mock.close(); + }) + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should allow to query for notifications', () => { + const observer = createSpyObserver(); + service.query("updates:").subscribe(observer); + + mock.expectLastMessage() + mock.expectLastMessage('type').toBe('query') + mock.expectLastMessage('query').toBe('notifications:all/updates:') + + mock.lastMultiplex!.next({ + id: mock.lastRequestId!, + type: 'ok', + data: { + ID: 'updates:core-update-available', + Message: 'Update available', + }, + key: 'notifications:all/updates:core-update-available' + }) + + mock.lastMultiplex!.next({ + id: mock.lastRequestId!, + type: 'ok', + data: { + ID: 'updates:ui-reload-required', + Message: 'UI reload required', + }, + key: 'notifications:all/updates:ui-reload-required' + }) + + // query collects all notifications using toArray + // so nothing should be nexted yet. + expect(observer.next).not.toHaveBeenCalled() + expect(observer.error).not.toHaveBeenCalled() + expect(observer.complete).not.toHaveBeenCalled() + + // finish the strea + mock.lastMultiplex!.next({ + id: mock.lastRequestId!, + type: 'done' + }) + + expect(observer.next).toHaveBeenCalledWith([ + { + ID: 'updates:core-update-available', + Message: 'Update available', + }, + { + ID: 'updates:ui-reload-required', + Message: 'UI reload required', + } + ]) + expect(observer.error).not.toHaveBeenCalled() + expect(observer.complete).toHaveBeenCalled() + }); + + describe('execute notification actions', () => { + it('should work using a notif object', () => { + let observer = createSpyObserver(); + let notif: any = { + ID: 'updates:core-update-available', + Message: 'An update is available', + Type: NotificationType.Info, + AvailableActions: [{ ID: "restart", Text: "Restart" }], + } + + service.execute(notif, "restart").subscribe(observer); + + expect(observer.error).not.toHaveBeenCalled() + + mock.expectLastMessage('type').toBe('update'); + mock.expectLastMessage('key').toBe('notifications:all/updates:core-update-available'); + mock.expectLastMessage('data').toEqual({ + ID: 'updates:core-update-available', + SelectedActionID: 'restart', + }); + + mock.lastMultiplex!.next({ + id: mock.lastRequestId!, + type: 'success' + }) + + expect(observer.next).toHaveBeenCalledWith(undefined); + expect(observer.error).not.toHaveBeenCalled(); + expect(observer.complete).toHaveBeenCalled(); + }); + + it('should throw when executing an unknown action using a notif object', () => { + let observer = createSpyObserver(); + let notif: any = { + ID: 'updates:core-update-available', + Message: 'An update is available', + Type: NotificationType.Info, + AvailableActions: [{ ID: "restart", Text: "Restart" }], + } + + service.execute(notif, "restart-with-typo").subscribe(observer); + + expect(observer.error).toHaveBeenCalled() + expect(mock.lastMessageSent).toBeUndefined(); + }); + + it('should work using a key', () => { + let observer = createSpyObserver(); + service.execute("updates:core-update-available", "restart").subscribe(observer); + + expect(observer.error).not.toHaveBeenCalled() + + mock.expectLastMessage('type').toBe('update'); + mock.expectLastMessage('key').toBe('notifications:all/updates:core-update-available'); + mock.expectLastMessage('data').toEqual({ + ID: 'updates:core-update-available', + SelectedActionID: 'restart', + }); + + mock.lastMultiplex!.next({ + id: mock.lastRequestId!, + type: 'success' + }) + + expect(observer.next).toHaveBeenCalledWith(undefined); + expect(observer.error).not.toHaveBeenCalled(); + expect(observer.complete).toHaveBeenCalled(); + }); + }) + + describe('resolving pending actions', () => { + it('should work using a notif object', () => { + let observer = createSpyObserver(); + let notif: any = { + ID: 'updates:core-update-available', + Message: 'An update is available', + Type: NotificationType.Info, + Responded: Math.round(Date.now() / 1000), + SelectedActionID: "restart", + } + + service.resolvePending(notif, 100).subscribe(observer) + + expect(observer.error).not.toHaveBeenCalled() + + mock.expectLastMessage('type').toBe('update'); + mock.expectLastMessage('key').toBe('notifications:all/updates:core-update-available'); + mock.expectLastMessage('data').toEqual({ + ID: 'updates:core-update-available', + Executed: 100, + }); + + mock.lastMultiplex!.next({ + id: mock.lastRequestId!, + type: 'success' + }) + + expect(observer.next).toHaveBeenCalledWith(undefined); + expect(observer.error).not.toHaveBeenCalled(); + expect(observer.complete).toHaveBeenCalled(); + }); + + it('should throw on an executed notification using a notif object', () => { + let observer = createSpyObserver(); + let notif: any = { + ID: 'updates:core-update-available', + Message: 'An update is available', + Type: NotificationType.Info, + SelectedActionID: 'restart', + Responded: Math.round(Date.now() / 1000), + Executed: Math.round(Date.now() / 1000), + } + + service.resolvePending(notif).subscribe(observer); + + expect(observer.error).toHaveBeenCalled() + expect(mock.lastMessageSent).toBeUndefined(); + }); + + it('should work using a key', () => { + let observer = createSpyObserver(); + service.resolvePending("updates:core-update-available", 100).subscribe(observer); + + expect(observer.error).not.toHaveBeenCalled() + + mock.expectLastMessage('type').toBe('update'); + mock.expectLastMessage('key').toBe('notifications:all/updates:core-update-available'); + mock.expectLastMessage('data').toEqual({ + ID: 'updates:core-update-available', + Executed: 100, + }); + + mock.lastMultiplex!.next({ + id: mock.lastRequestId!, + type: 'success' + }) + + expect(observer.next).toHaveBeenCalledWith(undefined); + expect(observer.error).not.toHaveBeenCalled(); + expect(observer.complete).toHaveBeenCalled(); + }); + }); + + describe('watching notifications', () => { + it('should be possible to watch for new and action-required notifs only', () => { + const observer = createSpyObserver(); + service.new$.subscribe(observer); + + let send = (msg: any) => { + mock.lastMultiplex!.next({ + id: mock.lastRequestId!, + data: msg, + type: 'ok', + key: "notifications:all/" + msg.ID, + }) + } + + let n1 = { + ID: "new-notif-1", + Message: "a new notification", + Responded: 0, + Executed: 0, + Expires: Math.round(Date.now() / 1000) + 60 * 60, + } + let n2 = { + ID: "new-notif-2", + Message: "a new notification", + Responded: 0, + Executed: 0, + Expires: 0, + AvailableActions: [{ ID: "action-id", Text: "some action" }], + } + let expired = { + ID: "new-notif-3", + Message: "a new notification", + Responded: 0, + Executed: 0, + Expires: 100, + } + let pending = { + ID: "new-notif-4", + Message: "a new notification", + Responded: Math.round(Date.now() / 1000), + Executed: 0, + SelectedActionID: "test", + } + + send(n1) + send(expired) + send(n2) + send(pending) + + expect(observer.complete).not.toHaveBeenCalled() + expect(observer.error).not.toHaveBeenCalled() + expect(observer.next).toHaveBeenCalledTimes(2) + expect(observer.next).toHaveBeenCalledWith(n1) + expect(observer.next).toHaveBeenCalledWith(n2) + }) + }) + + describe('creating notifications', () => { + it('should be possible using an object', () => { + let notification: Partial> = { + ID: 'my-awesome-notification', + AvailableActions: [ + { ID: 'action-no', Text: 'No' }, + { ID: 'force-no', Text: 'Hell No' } + ], + Message: 'Update complete, do you want to reboot?', + Persistent: true, + Type: NotificationType.Warning, + } + + let observer = createSpyObserver(); + service.create(notification).subscribe(observer); + + expect(observer.error).not.toHaveBeenCalled(); + + mock.expectLastMessage('type').toBe('create') + mock.expectLastMessage('key').toBe('notifications:all/my-awesome-notification') + mock.expectLastMessage('data').toEqual(notification); + expect(notification.Created).toBeTruthy(); + + mock.lastMultiplex!.next({ + type: 'success', + id: mock.lastRequestId!, + }) + + expect(observer.complete).toHaveBeenCalled() + expect(observer.error).not.toHaveBeenCalled() + expect(observer.next).toHaveBeenCalledWith(undefined) + }) + + it('should be possible using parameters', () => { + let observer = createSpyObserver(); + service.create('my-param-notification', 'message', NotificationType.Prompt, { + Persistent: true, + Created: 100, + }).subscribe(observer); + + expect(observer.error).not.toHaveBeenCalled(); + + mock.expectLastMessage('type').toBe('create') + mock.expectLastMessage('key').toBe('notifications:all/my-param-notification') + mock.expectLastMessage('data').toEqual({ + Type: NotificationType.Prompt, + ID: 'my-param-notification', + Message: 'message', + Created: 100, + Persistent: true, + }); + + mock.lastMultiplex!.next({ + type: 'success', + id: mock.lastRequestId!, + }) + + expect(observer.complete).toHaveBeenCalled() + expect(observer.error).not.toHaveBeenCalled() + expect(observer.next).toHaveBeenCalledWith(undefined) + + }) + }) +}); + +function createSpyObserver(): PartialObserver { + return jasmine.createSpyObj("observer", ["next", "error", "complete"]) +} diff --git a/desktop/angular/src/app/services/notifications.service.ts b/desktop/angular/src/app/services/notifications.service.ts new file mode 100644 index 00000000..b15949f2 --- /dev/null +++ b/desktop/angular/src/app/services/notifications.service.ts @@ -0,0 +1,395 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable, TrackByFunction, inject } from '@angular/core'; +import { Params, Router } from '@angular/router'; +import { PortapiService, RetryableOpts } from '@safing/portmaster-api'; +import { BehaviorSubject, Observable, combineLatest, defer, throwError } from 'rxjs'; +import { map, share, toArray } from 'rxjs/operators'; +import { environment } from 'src/environments/environment'; +import { ActionIndicatorService } from '../shared/action-indicator'; +import { Action, ActionHandler, NetqueryAction, Notification, NotificationState, NotificationType, OpenPageAction, OpenProfileAction, OpenSettingAction, OpenURLAction, PageIDs, WebhookAction } from './notifications.types'; +import { VirtualNotification } from './virtual-notification'; +import { INTEGRATION_SERVICE } from '../integration'; + +@Injectable({ + providedIn: 'root' +}) +export class NotificationsService { + private readonly integration = inject(INTEGRATION_SERVICE); + + /** + * A {@link TrackByFunction} from tracking notifications. + */ + static trackBy: TrackByFunction> = function (_: number, n: Notification) { + return n.EventID; + }; + + /** + * This object contains handler methods for all + * notification action types we currently support. + */ + private actionHandler: { + [key in Action['Type']]: (a: any) => Promise; + } = { + '': async () => { }, + 'open-url': async (a: OpenURLAction) => { + await this.integration.openExternal(a.Payload); + }, + 'open-profile': (a: OpenProfileAction) => this.router.navigate([ + '/app', ...a.Payload.split('/') + ]), + 'open-setting': (a: OpenSettingAction) => { + if (a.Payload.Profile) { + return this.router.navigate(['/app', ...a.Payload.Profile.split('/')], { + queryParams: { + setting: a.Payload.Key, + tab: 'settings' + } + }) + } + return this.router.navigate(['/settings'], { + queryParams: { + setting: a.Payload.Key + } + }) + }, + "open-page": (a: OpenPageAction) => { + let pageID: keyof typeof PageIDs | null = null; + let queryParams: Params | null = null; + + if (typeof a.Payload === 'string') { + pageID = a.Payload; + queryParams = {}; + } else { + pageID = a.Payload.id; + queryParams = a.Payload.query; + } + + const url = PageIDs[pageID]; + if (!!url) { + return this.router.navigate([url], { + queryParams, + }) + } + return Promise.reject('not yet supported'); + }, + "ui": (a: ActionHandler) => { + return a.Run(a); + }, + "netquery": (a: NetqueryAction) => { + return this.router.navigate(['/monitor'], { + queryParams: { + q: a.Payload, + } + }) + }, + "call-webhook": (a: WebhookAction) => { + let method = a.Payload.Method; + if (method === '') { + if (a.Payload.Payload !== undefined && a.Payload.Payload !== null) { + method = 'PUT' + } else { + method = 'POST' + } + } + let req = this.http.request( + method, + `${environment.httpAPI}/v1/${a.Payload.URL}`, + { + body: a.Payload.Payload, + observe: 'response', + responseType: 'arraybuffer', + } + ) + return new Promise((resolve, reject) => { + const observer = this.actionIndicator.httpObserver(); + req.subscribe({ + next: res => { + if (a.Payload.ResultAction === 'display') { + if (!!observer?.next) { + observer.next(res) + } + } + resolve(res); + }, + error: err => { + if (!!observer?.error) { + observer.error(err); + } + reject(err); + }, + }) + }) + } + }; + + // For testing purposes only + VirtualNotification = VirtualNotification; + + /** A map of virtual notifications */ + private _virtualNotifications = new Map>(); + + /* Emits all virtual notifications whenever they change */ + private _virtualNotificationChange = new BehaviorSubject[]>([]); + + /* A copy of the static trackBy function. */ + trackBy = NotificationsService.trackBy; + + /** The prefix that all notifications have */ + readonly notificationPrefix = "notifications:all/"; + + /** new$ emits new (active) notifications as they arrive */ + readonly new$: Observable[]>; + + constructor( + private portapi: PortapiService, + private router: Router, + private http: HttpClient, + private actionIndicator: ActionIndicatorService, + ) { + this.new$ = this.watchAll().pipe( + src => this.injectVirtual(src), + map(msgs => { + return msgs.filter(msg => msg.State === NotificationState.Active || !msg.State) + }), + share({ connector: () => new BehaviorSubject[]>([]) }) + ); + } + + /** + * Inject a new virtual notification. If not configured otherwise, + * the notification is automatically removed when executed. + */ + inject(notif: VirtualNotification, { autoRemove } = { autoRemove: true }) { + this._virtualNotifications.set(notif.EventID, notif); + this._virtualNotificationChange.next( + Array.from(this._virtualNotifications.values()) + ) + + if (autoRemove) { + notif.executed.subscribe({ complete: () => this.deject(notif) }); + } + } + + /** Deject (remove) a virtual notification. */ + deject(notif: VirtualNotification) { + this._virtualNotifications.delete(notif.EventID); + + this._virtualNotificationChange.next( + Array.from(this._virtualNotifications.values()) + ) + } + + /** A {@link MonoOperatorFunction} that injects all virtual observables into the source. */ + private injectVirtual(obs: Observable[]>): Observable { + return combineLatest([ + obs, + this._virtualNotificationChange, + ]).pipe( + map(([real, virtual]) => { + return [ + ...real, + ...virtual, + ] + }) + ) + } + + /** + * Watch all notifications that match a query. + * + * + * @param query The query to watch. Defaulta to all notifcations + * @param opts Optional retry configuration options. + */ + watchAll(query: string = '', opts?: RetryableOpts): Observable[]> { + return this.portapi.watchAll>(this.notificationPrefix + query, opts); + } + + /** + * Query the backend for a list of notifications. In contrast + * to {@class PortAPI} query collects all results into an array + * first which makes it convenient to be used in *ngFor and + * friends. See {@function trackNotification} for a suitable track-by + * function. + * + * @param query The search query. + */ + query(query: string): Observable[]> { + return this.portapi.query>(this.notificationPrefix + query) + .pipe( + map(value => value.data), + toArray() + ) + } + + /** + * Returns the notification by ID. + * + * @param id The ID of the notification + */ + get(id: string): Observable> { + return this.portapi.get(this.notificationPrefix + id) + } + + /** + * Execute an action attached to a notification. + * + * @param n The notification object. + * @param actionId The ID of the action to execute. + */ + execute(n: Notification, action: Action): Observable; + + /** + * Execute an action attached to a notification. + * + * @param notificationId The ID of the notification. + * @param actionId The ID of the action to execute. + */ + execute(notificationId: string, action: Action): Observable; + + // overloaded implementation of execute + execute(notifOrId: Notification | string, action: Action): Observable { + const payload: Partial> = {}; + if (typeof notifOrId === 'string') { + payload.EventID = notifOrId; + } else { + payload.EventID = notifOrId.EventID; + } + + // if it's a virtual notification we should let it handle the action + // on it's own. + if (!!this._virtualNotifications.get(payload.EventID)) { + return defer(async () => { + const notif = this._virtualNotifications.get(payload.EventID!); + if (!!notif) { + notif.selectAction(action.ID); + } + }) + } + + return defer(async () => { + try { + await this.performAction(action); + + // finally, if there's an action ID, mark the notification as resolved. + if (!!action.ID) { + payload.SelectedActionID = action.ID; + const key = this.notificationPrefix + payload.EventID; + await this.portapi.update(key, payload).toPromise(); + } + } catch (err: any) { + const msg = this.actionIndicator.getErrorMessgae(err); + this.actionIndicator.error('Internal Error', 'Failed to perform action: ' + msg) + } + }) + } + + async performAction(action: Action) { + // if there's an action type defined execute the handler. + if (!!action.Type) { + const handler = this.actionHandler[action.Type] as (a: Action) => Promise; + if (!!handler) { + console.log(action); + await handler(action); + } else { + this.actionIndicator.error('Internal Error', 'Cannot handle action type ' + action.Type) + } + } + } + + /** + * Resolve a pending notification execution. + * + * @param n The notification object to resolve the pending execution. + * @param time optional The time at which the pending execution took place + */ + resolvePending(n: Notification, time?: number): Observable; + + /** + * Resolve a pending notification execution. + * + * @param n The notification ID to resolve the pending execution. + * @param time optional The time at which the pending execution took place + */ + resolvePending(n: string, time?: number): Observable; + + // overloaded implementation of resolvePending. + resolvePending(notifOrID: Notification | string, time: number = (Math.round(Date.now() / 1000))): Observable { + const payload: Partial> = {}; + if (typeof notifOrID === 'string') { + payload.EventID = notifOrID; + } else { + payload.EventID = notifOrID.EventID; + if (notifOrID.State === NotificationState.Executed) { + return throwError(`Notification ${notifOrID.EventID} already executed`); + } + } + + payload.State = NotificationState.Responded; + const key = this.notificationPrefix + payload.EventID + return this.portapi.update(key, payload); + } + + /** + * Delete a notification. + * + * @param n The notification to delete. + */ + delete(n: Notification): Observable; + + /** + * Delete a notification. + * + * @param n The notification to delete. + */ + delete(id: string): Observable; + + // overloaded implementation of delete. + delete(notifOrId: Notification | string): Observable { + return this.portapi.delete(typeof notifOrId === 'string' ? notifOrId : notifOrId.EventID); + } + + /** + * Create a new notification. + * + * @param n The notification to create. + */ + create(n: Partial>): Observable; + + /** + * Create a new notification. + * + * @param id The ID of the notificaiton. + * @param message The default message of the notificaiton. + * @param type The notification type + * @param args Additional arguments for the notification. + */ + create(id: string, message: string, type: NotificationType, args?: Partial>): Observable; + + // overloaded implementation of create. + create(notifOrId: Partial> | string, message?: string, type?: NotificationType, args?: Partial>): Observable { + if (typeof notifOrId === 'string') { + notifOrId = { + ...args, + EventID: notifOrId, + State: NotificationState.Active, + Message: message, + Type: type, + } as Notification; // it's actual Partial but that's fine. + } + + if (!notifOrId.EventID) { + return throwError(`Notification ID is required`); + } + + if (!notifOrId.Message) { + return throwError(`Notification message is required`); + } + + if (typeof notifOrId.Type !== 'number') { + return throwError(`Notification type is required`); + } + + return this.portapi.create(this.notificationPrefix + notifOrId.EventID, notifOrId); + } +} diff --git a/desktop/angular/src/app/services/notifications.types.ts b/desktop/angular/src/app/services/notifications.types.ts new file mode 100644 index 00000000..7ddf7029 --- /dev/null +++ b/desktop/angular/src/app/services/notifications.types.ts @@ -0,0 +1,205 @@ +import { getEnumKey, IntelEntity, Record } from '@safing/portmaster-api'; + +/** + * BaseAction defines a user selectable action and can + * be attached to a notification. Once selected, + * the action's ID is set as the SelectedActionID + * of the notification. + */ +export interface BaseAction { + // ID uniquely identifies the action. It's safe to + // use ID to select a localizable template to use + // instead of the Text property. If Type is set + // to None the ID may be empty, signifying that this + // action is merely to dismiss the notification. + ID: string; + // Text is the (default) text for the action label. + Text: string; +} + +export interface GenericAction extends BaseAction { + Type: ''; +} + +export interface OpenURLAction extends BaseAction { + Type: 'open-url'; + Payload: string; +} + +export interface OpenPageAction extends BaseAction { + Type: 'open-page'; + Payload: keyof typeof PageIDs | { + id: keyof typeof PageIDs, + query: { + [key: string]: string, + } + }; +} + +export interface NetqueryAction extends BaseAction { + Type: 'netquery'; + Payload: string; +} + +/** + * PageIDs holds a list of pages that can be opened using + * the OpenPageAction. + */ +export const PageIDs = { + 'monitor': '/monitor', + 'support': '/support', + 'settings': '/settings', + 'apps': '/app/overview', + 'spn': '/spn', +} + +export interface OpenSettingAction extends BaseAction { + Type: 'open-setting'; + Payload: { + Key: string; + Profile?: string; + } +} + +export interface OpenProfileAction extends BaseAction { + Type: 'open-profile'; + Payload: string; +} + +export interface WebhookAction extends BaseAction { + Type: 'call-webhook'; + Payload: { + Method: string; + URL: string; + Payload: any; + ResultAction: 'ignore' | 'display'; + } +} + +export interface ActionHandler extends BaseAction { + Type: 'ui' + Run: (vn: T) => Promise; + Payload: T; +} + +export type Action = GenericAction + | OpenURLAction + | OpenPageAction + | OpenSettingAction + | OpenProfileAction + | WebhookAction + | NetqueryAction + | ActionHandler; + +/** All action types that perform in-application routing. */ +export const routingActions = new Set([ + 'open-page', + 'open-profile', + 'open-setting' +]) + +/** + * Available types of notifications. Notification + * types are mainly for filtering and style related + * decisions. + */ +export enum NotificationType { + // Info is an informational message only. + Info = 0, + // Warning is a warning message. + Warning = 1, + // Prompt asks the user for a decision. + Prompt = 2, + // Error is for error notifications and module + // failure status. + Error = 3, +} + +export interface ConnectionPromptData { + Profile: { + ID: string; + LinkedPath: string; + Source: 'local'; + }; + Entity: IntelEntity; +} + +/** + * Returns a string representation of the notifcation type. + * + * @param val The notifcation type + */ +export function getNotificationTypeString(val: NotificationType): string { + return getEnumKey(NotificationType, val) +} + +/** + * Each notification can be in one of six different states + * that inform the client on how to handle the notification. + */ +export enum NotificationState { + // Active describes a notification that is active, no expired and, + // if actions are available, still waits for the user to select an + // action. + Active = "active", + // Responded describes a notification where the user has already + // selected which action to take but that action is still to be + // performed. + Responded = "responded", + // Responded describes a notification where the user has already + // selected which action to take but that action is still to be + // performed. + Executed = "executed", + // Invalid is a UI-only state that is used when the state of a + // notification is unknown. + Invalid = "invalid", +} + +export interface Notification extends Record { + // EventID is used to identify a specific notification. It consists of + // the module name and a per-module unique event id. + // The following format is recommended: + // : + EventID: string; + // GUID is a unique identifier for each notification instance. That is + // two notifications with the same EventID must still have unique GUIDs. + // The GUID is mainly used for system (Windows) integration and is + // automatically populated by the notification package. Average users + // don't need to care about this field. + GUID: string; + // Type is the notification type. It can be one of Info, Warning or Prompt. + Type: NotificationType; + // Message is the default message shown to the user if no localized version + // of the notification is available. Note that the message should already + // have any paramerized values replaced. Message may be formatted using + // markdown. + Message: string; + // Title holds a short notification title that quickly informs the user + // about the type of notification. + Title: string; + // Category holds an informative category for the notification and is mainly + // used for presentation purposes. + Category: string; + // EventData contains an additional payload for the notification. This payload + // may contain contextual data and may be used by a localization framework + // to populate the notification message template. + // If EventData implements sync.Locker it will be locked and unlocked together with the + // notification. Otherwise, EventData is expected to be immutable once the + // notification has been saved and handed over to the notification or database package. + EventData: T | null; + // Expires holds the unix epoch timestamp at which the notification expires + // and can be cleaned up. + // Users can safely ignore expired notifications and should handle expiry the + // same as deletion. + Expires: number; + // State describes the current state of a notification. See State for + // a list of available values and their meaning. + State: NotificationState; + // AvailableActions defines a list of actions that a user can choose from. + AvailableActions: Action[]; + // SelectedActionID is updated to match the ID of one of the AvailableActions + // based on the user selection. + SelectedActionID: string; +} + +export type ConnectionPrompt = Notification; diff --git a/desktop/angular/src/app/services/package.json b/desktop/angular/src/app/services/package.json new file mode 100644 index 00000000..a4382915 --- /dev/null +++ b/desktop/angular/src/app/services/package.json @@ -0,0 +1,3 @@ +{ + "sideEffects": false +} diff --git a/desktop/angular/src/app/services/session-data.service.ts b/desktop/angular/src/app/services/session-data.service.ts new file mode 100644 index 00000000..cb88ea91 --- /dev/null +++ b/desktop/angular/src/app/services/session-data.service.ts @@ -0,0 +1,72 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { distinctUntilChanged, map } from 'rxjs/operators'; + +/** + * SessionDataService is used to store transient data + * that are only important as long as the application is + * being used. Those data are not presisted and are + * removed once the application is restarted. + */ +@Injectable({ + providedIn: 'root' +}) +export class SessionDataService { + private data = new Map(); + private stream = new BehaviorSubject(undefined); + + /** Set sets a value in the session data service */ + set(key: string, value: T): void { + this.data.set(key, value); + } + + get(key: string): T | null; + get(key: string, def: T): T; + + /** Get retrieves a value from the session data service */ + get(key: string, def?: any): any { + const value = this.data.get(key); + if (value !== undefined) { + return value; + } + + if (def !== undefined) { + return def; + } + return null; + } + + watch(key: string): Observable; + watch(key: string, def: T): Observable; + + /** Watch a key for changes to it's identity. */ + watch(key: string, def?: any): Observable { + return this.stream + .pipe( + map(() => this.get(key, def)), + distinctUntilChanged() + ); + } + + delete(key: string): T | null { + let value = this.get(key); + if (value !== null) { + this.data.delete(key); + } + return value; + } + + save(id: string, model: M, keys: K[]) { + let copy: Partial = {}; + keys.forEach(key => copy[key] = model[key]); + this.set(id, copy); + } + + restore(id: string, model: M) { + let copy: Partial | null = this.get(id); + if (copy === null) { + return; + } + Object.assign(model, copy); + } +} diff --git a/desktop/angular/src/app/services/status.service.spec.ts b/desktop/angular/src/app/services/status.service.spec.ts new file mode 100644 index 00000000..34ce8a6f --- /dev/null +++ b/desktop/angular/src/app/services/status.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { StatusService } from './status.service'; + +describe('StatusService', () => { + let service: StatusService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(StatusService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/desktop/angular/src/app/services/status.service.ts b/desktop/angular/src/app/services/status.service.ts new file mode 100644 index 00000000..c52b73c7 --- /dev/null +++ b/desktop/angular/src/app/services/status.service.ts @@ -0,0 +1,95 @@ +import { Injectable, TrackByFunction } from '@angular/core'; +import { PortapiService, RetryableOpts, SecurityLevel, WatchOpts, trackById } from '@safing/portmaster-api'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { filter, map, repeat, share, toArray } from 'rxjs/operators'; +import { CoreStatus, Subsystem, VersionStatus } from './status.types'; + +@Injectable({ + providedIn: 'root' +}) +export class StatusService { + /** + * A {@link TrackByFunction} from tracking subsystems. + */ + static trackSubsystem: TrackByFunction = trackById; + readonly trackSubsystem = StatusService.trackSubsystem; + + readonly statusPrefix = "runtime:" + readonly subsystemPrefix = this.statusPrefix + "subsystems/" + + /** + * status$ watches the global core status. It's mutlicasted using a BehaviorSubject so new + * subscribers will automatically get the latest version while only one subscription + * to the backend is held. + */ + readonly status$: Observable = this.portapi.qsub(`runtime:system/status`) + .pipe( + repeat({ delay: 2000 }), + map(reply => reply.data), + share({ connector: () => new BehaviorSubject(null) }), + filter(value => value !== null), + ) as Observable; // we filtered out the null values but we cannot make that typed with RxJS. + + constructor(private portapi: PortapiService) { } + + /** Returns the currently available versions for all resources. */ + getVersions(): Observable { + return this.portapi.get('core:status/versions') + } + + /** + * Selectes a new security level. SecurityLevel.Off means that + * the auto-pilot should take over. + * + * @param securityLevel The security level to select + */ + selectLevel(securityLevel: SecurityLevel): Observable { + return this.portapi.update(`runtime:system/security-level`, { + SelectedSecurityLevel: securityLevel, + }); + } + + + /** + * Loads the current status of a subsystem. + * + * @param name The ID of the subsystem + */ + getSubsystemStatus(id: string): Observable { + return this.portapi.get(this.subsystemPrefix + id); + } + + /** + * Loads the current status of all subsystems matching idPrefix. + * If idPrefix is an empty string all subsystems are returned. + * + * @param idPrefix An optional ID prefix to limit the returned subsystems + */ + querySubsystem(idPrefix: string = ''): Observable { + return this.portapi.query(this.subsystemPrefix + idPrefix) + .pipe( + map(reply => reply.data), + toArray(), + ) + } + + /** + * Watch a subsystem for changes. Completes when the subsystem is + * deleted. See {@method PortAPI.watch} for more information. + * + * @param id The ID of the subsystem to watch. + * @param opts Additional options for portapi.watch(). + */ + watchSubsystem(id: string, opts?: WatchOpts): Observable { + return this.portapi.watch(this.subsystemPrefix + id, opts); + } + + /** + * Watch for subsystem changes + * + * @param opts Additional options for portapi.sub(). + */ + watchSubsystems(opts?: RetryableOpts): Observable { + return this.portapi.watchAll(this.subsystemPrefix, opts); + } +} diff --git a/desktop/angular/src/app/services/status.types.ts b/desktop/angular/src/app/services/status.types.ts new file mode 100644 index 00000000..f5188366 --- /dev/null +++ b/desktop/angular/src/app/services/status.types.ts @@ -0,0 +1,132 @@ +import { getEnumKey, Record, ReleaseLevel, SecurityLevel } from '@safing/portmaster-api'; + +export interface CaptivePortal { + URL: string; + IP: string; + Domain: string; +} + +export enum ModuleStatus { + Off = 0, + Error = 1, + Warning = 2, + Operational = 3 +} + +/** + * Returns a string represetnation of the module status. + * + * @param stat The module status to translate + */ +export function getModuleStatusString(stat: ModuleStatus): string { + return getEnumKey(ModuleStatus, stat) +} + +export enum OnlineStatus { + Unknown = 0, + Offline = 1, + Limited = 2, // local network only, + Portal = 3, + SemiOnline = 4, + Online = 5, +} + +/** + * Converts a online status value to a string. + * + * @param stat The online status value to convert + */ +export function getOnlineStatusString(stat: OnlineStatus): string { + return getEnumKey(OnlineStatus, stat) +} + +export interface Threat { + ID: string; + Name: string; + Description: string; + AdditionalData: T; + MitigationLevel: SecurityLevel; + Started: number; + Ended: number; +} + +export interface CoreStatus extends Record { + ActiveSecurityLevel: SecurityLevel; + SelectedSecurityLevel: SecurityLevel; + ThreatMitigationLevel: SecurityLevel; + OnlineStatus: OnlineStatus; + Threats: Threat[]; + CaptivePortal: CaptivePortal; +} + +export enum FailureStatus { + Operational = 0, + Hint = 1, + Warning = 2, + Error = 3 +} + +/** + * Returns a string representation of a failure status value. + * + * @param stat The failure status value. + */ +export function getFailureStatusString(stat: FailureStatus): string { + return getEnumKey(FailureStatus, stat) +} + +export interface Module { + Enabled: boolean; + FailureID: string; + FailureMsg: string; + FailureStatus: FailureStatus; + Name: string; + Status: ModuleStatus; +} + +export interface Subsystem extends Record { + ConfigKeySpace: string; + Description: string; + ExpertiseLevel: string; + FailureStatus: FailureStatus; + ID: string; + Modules: Module[]; + Name: string; + ReleaseLevel: ReleaseLevel; + ToggleOptionKey: string; +} + +export interface CoreVersion { + BuildDate: string; + BuildHost: string; + BuildOptions: string; + BuildSource: string; + BuildUser: string; + Commit: string; + License: string; + Name: string; + Version: string; +} + +export interface ResourceVersion { + Available: boolean; + BetaRelease: boolean; + Blacklisted: boolean; + StableRelease: boolean; + VersionNumber: string; +} + +export interface Resource { + ActiveVersion: ResourceVersion | null; + Identifier: string; + SelectedVersion: ResourceVersion; + Versions: ResourceVersion[]; +} + +export interface VersionStatus extends Record { + Channel: string; + Core: CoreVersion; + Resources: { + [key: string]: Resource + } +} diff --git a/desktop/angular/src/app/services/supporthub.service.ts b/desktop/angular/src/app/services/supporthub.service.ts new file mode 100644 index 00000000..9b8cfd7c --- /dev/null +++ b/desktop/angular/src/app/services/supporthub.service.ts @@ -0,0 +1,82 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { environment } from 'src/environments/environment'; + +export interface SupportSection { + title: string; + body: string; +} + +export interface Issue { + title: string; + body: string; + createdAt: CreatedAt; + repository: string; + url: string; + user: string; + closed?: boolean; + labels: string[]; +} + +@Injectable({ providedIn: 'root' }) +export class SupportHubService { + constructor(private http: HttpClient) { } + + loadIssues(): Observable { + interface LoadIssuesResponse { + issues: Issue[]; + } + return this.http.get(`${environment.supportHub}/api/v1/issues`) + .pipe(map(res => res.issues.map(issue => ({ + ...issue, + createdAt: new Date(issue.createdAt), + })).reverse())); + } + + /** Uploads content under name */ + uploadText(name: string, content: string): Observable { + interface UploadResponse { + urls: { + [key: string]: string[]; + } + } + const blob = new Blob([content], { type: 'text/plain' }); + const data = new FormData(); + data.set("file", blob, name); + + return this.http.post(`${environment.supportHub}/api/v1/upload`, data) + .pipe(map(res => res.urls['file'][0])); + } + + /** Create github issue */ + createIssue(repo: string, preset: string, title: string, sections: SupportSection[], debugInfoUrl?: string, opts?: { + generateUrl: boolean, + }): Observable { + interface CreateIssueResponse { + url: string; + } + const req = { + title, + sections, + debugInfoUrl + } + let params = new HttpParams(); + if (!!opts?.generateUrl) { + params = params.set('generate-url', '') + } + return this.http.post(`${environment.supportHub}/api/v1/issues/${repo}/${preset}`, req, { params }).pipe(map(r => r.url)) + } + + createTicket(repoName: string, title: string, email: string, sections: SupportSection[], debugInfoUrl?: string): Observable { + const req = { + title, + sections, + debugInfoUrl, + email, + repoName, + } + return this.http.post(`${environment.supportHub}/api/v1/ticket`, req) + } +} diff --git a/desktop/angular/src/app/services/ui-state.service.ts b/desktop/angular/src/app/services/ui-state.service.ts new file mode 100644 index 00000000..25ad4d09 --- /dev/null +++ b/desktop/angular/src/app/services/ui-state.service.ts @@ -0,0 +1,57 @@ +import { Injectable } from "@angular/core"; +import { PortapiService, Record } from '@safing/portmaster-api'; +import { Observable, of } from "rxjs"; +import { catchError, map, switchMap } from "rxjs/operators"; +import { SortTypes } from './../shared/network-scout/network-scout'; + +export interface UIState extends Record { + hideExitScreen?: boolean; + introScreenFinished?: boolean; + netscoutSortOrder: SortTypes; +} + +const defaultState: UIState = { + hideExitScreen: false, + introScreenFinished: false, + netscoutSortOrder: SortTypes.static +} + +@Injectable({ providedIn: 'root' }) +export class UIStateService { + constructor(private portapi: PortapiService) { } + + uiState(): Observable { + const key = 'core:ui/v1'; + return this.portapi.get(key) + .pipe( + catchError(err => of(defaultState)), + map(state => { + (Object.keys(defaultState) as (keyof UIState)[]) + .forEach(key => { + if (state[key] === undefined) { + (state as any)[key] = defaultState[key]! + } + }) + + return state + }) + ) + } + + saveState(state: UIState): Observable { + const key = 'core:ui/v1'; + return this.portapi.create(key, state); + } + + set(key: K, value: V): Observable { + return this.uiState() + .pipe( + map(state => { + state[key] = value + + return state; + }), + switchMap(newState => this.saveState(newState)) + ); + } +} diff --git a/desktop/angular/src/app/services/virtual-notification.ts b/desktop/angular/src/app/services/virtual-notification.ts new file mode 100644 index 00000000..592d886a --- /dev/null +++ b/desktop/angular/src/app/services/virtual-notification.ts @@ -0,0 +1,85 @@ +import { RecordMeta } from '@safing/portmaster-api'; +import { BehaviorSubject } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import { ActionHandler, Notification, NotificationState, NotificationType } from './notifications.types'; + +export class VirtualNotification implements Notification { + readonly AvailableActions: ActionHandler[]; + readonly Category: string; + readonly EventData: T | null; + readonly GUID: string = ''; // TODO(ppacher): should we fake it? + readonly Expires: number; + readonly _meta: RecordMeta; + + get State() { + if (this.SelectedActionID === '') { + return NotificationState.Active + } + + return NotificationState.Executed + } + + get SelectedActionID() { + return this._selectedAction.getValue(); + } + + /** Emits as soon as the user selects one of the notification actions. */ + get executed() { + return this._selectedAction.pipe( + filter(action => action !== '') + ); + } + + /* Used to emit the selected action */ + private _selectedAction = new BehaviorSubject(''); + + /** + * Select and execute the action by ID. + * + * @param aid The ID of the action to execute. + */ + selectAction(aid: string) { + this._selectedAction.next(aid); + this._meta.Modified = new Date().valueOf() / 1000; + + const action = this.AvailableActions.find(a => a.ID === aid); + if (!!action) { + action.Run(action.Payload); + } + } + + constructor( + public readonly EventID: string, + public readonly Type: NotificationType, + public readonly Title: string, + public readonly Message: string, + { + AvailableActions, + EventData, + Category, + Expires, + }: { + AvailableActions?: ActionHandler[]; + EventData?: T | null; + Category?: string, + Expires?: number, + } = {} + ) { + this.AvailableActions = AvailableActions || []; + this.EventData = EventData || null; + this.Category = Category || ''; + this.Expires = Expires || 0; + + this._meta = { + Created: new Date().valueOf() / 1000, + Deleted: 0, + Expires: this.Expires, + Modified: new Date().valueOf() / 1000, + Key: `notifications:all/${EventID}`, + } + } + + dispose() { + this._selectedAction.complete(); + } +} diff --git a/desktop/angular/src/app/shared/action-indicator/action-indicator.module.ts b/desktop/angular/src/app/shared/action-indicator/action-indicator.module.ts new file mode 100644 index 00000000..2f5dafcd --- /dev/null +++ b/desktop/angular/src/app/shared/action-indicator/action-indicator.module.ts @@ -0,0 +1,13 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { IndicatorComponent } from "./indicator"; + +@NgModule({ + imports: [ + CommonModule, + ], + declarations: [ + IndicatorComponent, + ] +}) +export class ActionIndicatorModule { } diff --git a/desktop/angular/src/app/shared/action-indicator/action-indicator.service.ts b/desktop/angular/src/app/shared/action-indicator/action-indicator.service.ts new file mode 100644 index 00000000..143168e1 --- /dev/null +++ b/desktop/angular/src/app/shared/action-indicator/action-indicator.service.ts @@ -0,0 +1,284 @@ +import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { Injectable, InjectionToken, Injector, isDevMode } from '@angular/core'; +import { interval, PartialObserver, Subject } from 'rxjs'; +import { take, takeUntil } from 'rxjs/operators'; +import { IndicatorComponent } from './indicator'; + +export interface ActionIndicator { + title: string; + message?: string; + status: 'info' | 'success' | 'error'; + timeout?: number; +} + +export const ACTION_REF = new InjectionToken('ActionIndicatorRef') +export class ActionIndicatorRef implements ActionIndicator { + title: string; + message?: string; + status: 'info' | 'success' | 'error'; + timeout?: number; + + onClose = new Subject(); + onCloseReplace = new Subject(); + + constructor(opts: ActionIndicator, private _overlayRef: OverlayRef) { + this.title = opts.title; + this.message = opts.message; + this.status = opts.status; + this.timeout = opts.timeout; + } + + close() { + this._overlayRef.detach(); + this.onClose.next(); + this.onClose.complete(); + } +} + +@Injectable({ providedIn: 'root' }) +export class ActionIndicatorService { + private _activeIndicatorRef: ActionIndicatorRef | null = null; + + constructor( + private _injector: Injector, + private overlay: Overlay, + ) { } + + /** + * Returns an observer that parses the HTTP API response + * and shows a success/error action indicator. + */ + httpObserver(successTitle?: string, errorTitle?: string): PartialObserver> { + return { + next: resp => { + let msg = this.getErrorMessgae(resp) + if (!successTitle) { + successTitle = msg; + msg = ''; + } + this.success(successTitle || '', msg) + }, + error: err => { + let msg = this.getErrorMessgae(err); + if (!errorTitle) { + errorTitle = msg; + msg = ''; + } + this.error(errorTitle || '', msg); + } + } + } + + info(title: string, message?: string, timeout?: number) { + this.create({ + title, + message: this.ensureMessage(message), + timeout, + status: 'info' + }) + } + + error(title: string, message?: string | any, timeout?: number) { + this.create({ + title, + message: this.ensureMessage(message), + timeout, + status: 'error' + }) + } + + success(title: string, message?: string, timeout?: number) { + this.create({ + title, + message: this.ensureMessage(message), + timeout, + status: 'success' + }) + } + + /** + * Creates a new user action indicator. + * + * @param msg The action indicator message to show + */ + async create(msg: ActionIndicator) { + if (!!this._activeIndicatorRef) { + this._activeIndicatorRef.onCloseReplace.next(); + await this._activeIndicatorRef.onClose.toPromise(); + } + + const cfg = new OverlayConfig({ + scrollStrategy: this.overlay + .scrollStrategies.noop(), + positionStrategy: this.overlay + .position() + .global() + .bottom('2rem') + .left('5rem'), + }); + const overlayRef = this.overlay.create(cfg); + + const ref = new ActionIndicatorRef(msg, overlayRef); + ref.onClose.pipe(take(1)).subscribe(() => { + if (ref === this._activeIndicatorRef) { + this._activeIndicatorRef = null; + } + }) + + // close after the specified time our (or 5000 seconds). + const timeout = msg.timeout || 5000; + interval(timeout).pipe( + takeUntil(ref.onClose), + take(1), + ).subscribe(() => { + ref.close(); + }) + + const injector = this.createInjector(ref); + const portal = new ComponentPortal( + IndicatorComponent, + undefined, + injector + ); + this._activeIndicatorRef = ref; + overlayRef.attach(portal); + } + + /** + * Creates a new dependency injector that provides msg as + * ACTION_MESSAGE. + */ + private createInjector(ref: ActionIndicatorRef): Injector { + return Injector.create({ + providers: [ + { + provide: ACTION_REF, + useValue: ref, + } + ], + parent: this._injector, + }) + } + + /** + * Tries to extract a meaningful error message from msg. + */ + private ensureMessage(msg: string | any): string | undefined { + if (msg === undefined || msg === null) { + return undefined; + } + + if (msg instanceof HttpErrorResponse) { + return msg.message; + } + + if (typeof msg === 'string') { + return msg; + } + + if (typeof msg === 'object') { + if ('message' in msg) { + return msg.message; + } + if ('error' in msg) { + return this.ensureMessage(msg.error); + } + if ('toString' in msg) { + return msg.toString(); + } + } + + return JSON.stringify(msg); + } + + /** + * Coverts an untyped body received by the HTTP API to a string. + */ + private stringifyBody(body: any): string { + if (typeof body === 'string') { + return body; + } + + if (body instanceof ArrayBuffer) { + return new TextDecoder('utf-8').decode(body); + } + + if (typeof body === 'object') { + return this.ensureMessage(body) || ''; + } + console.error('unsupported body', body); + + return ''; + } + + /** + * @deprecated use the version without a typo ... + */ + getErrorMessgae(resp: HttpResponse | HttpErrorResponse | Error): string { + return this.getErrorMessage(resp) + } + + /** + * Parses a HTTP or HTTP Error response and returns a + * message that can be displayed to the user. + */ + getErrorMessage(resp: HttpResponse | HttpErrorResponse | Error): string { + try { + let body: string | null = null; + + if (typeof resp === 'string') { + return resp + } + + if (resp instanceof Error) { + return resp.message; + } + + if (resp instanceof HttpErrorResponse) { + // A client-side or network error occured. + if (resp.error instanceof Error) { + body = resp.error.message; + } else { + body = this.stringifyBody(resp.error); + } + + if (!!body) { + body = body[0].toLocaleUpperCase() + body.slice(1) + return body + } + } + + + if (resp instanceof HttpResponse) { + let msg = ''; + const ct = resp.headers.get('content-type') || ''; + + body = this.stringifyBody(resp.body); + + if (/application\/json/.test(ct)) { + if (!!body) { + msg = body; + } + } else if (/text\/plain/.test(ct)) { + msg = body; + } + + // Make the first letter uppercase + if (!!msg) { + msg = msg[0].toLocaleUpperCase() + msg.slice(1) + return msg; + } + } + + console.error(`Unexpected error type`, resp) + + return `Unknown error: ${resp}` + + } catch (err: any) { + console.error(err) + return `Unknown error: ${resp}` + } + } +} diff --git a/desktop/angular/src/app/shared/action-indicator/index.ts b/desktop/angular/src/app/shared/action-indicator/index.ts new file mode 100644 index 00000000..e243b7a0 --- /dev/null +++ b/desktop/angular/src/app/shared/action-indicator/index.ts @@ -0,0 +1,2 @@ +export * from './action-indicator.service'; +export * from './action-indicator.module'; diff --git a/desktop/angular/src/app/shared/action-indicator/indicator.html b/desktop/angular/src/app/shared/action-indicator/indicator.html new file mode 100644 index 00000000..5691cd1e --- /dev/null +++ b/desktop/angular/src/app/shared/action-indicator/indicator.html @@ -0,0 +1,30 @@ + +
+ + + +
+
+ + + +
+
+
+

{{ ref.title }}

+ + + + + +
+ + {{ ref.message }} + +
+
diff --git a/desktop/angular/src/app/shared/action-indicator/indicator.scss b/desktop/angular/src/app/shared/action-indicator/indicator.scss new file mode 100644 index 00000000..6189798c --- /dev/null +++ b/desktop/angular/src/app/shared/action-indicator/indicator.scss @@ -0,0 +1,74 @@ +:host { + box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.75); + @apply bg-gray-200; + @apply p-4; + @apply rounded; + position: relative; + width: 20rem; + display: flex; + cursor: pointer; + border-left: 2px solid transparent; + + + .icon { + display: flex; + align-items: flex-start; + flex-shrink: 1; + margin-right: 1rem; + padding-top: 2px; + } + + &.error { + @apply border-yellow; + + .icon { + @apply text-yellow + } + } + + .indicator-content { + display: flex; + flex-direction: column; + align-items: flex-start; + + h1 { + font-size: 0.85rem; + font-weight: 500; + margin-bottom: 0; + } + + .message, + h1 { + flex-shrink: 0; + text-overflow: ellipsis; + } + + .message { + font-size: 0.7rem; + flex-grow: 1; + opacity: .5; + + span { + display: block; + height: 100%; + word-break: keep-all; + } + } + + .close-icon { + position: absolute; + top: 1rem; + right: 1rem; + opacity: .7; + cursor: pointer; + + &:hover { + opacity: 1; + } + } + + h1~.message { + margin-top: .5rem; + } + } +} diff --git a/desktop/angular/src/app/shared/action-indicator/indicator.ts b/desktop/angular/src/app/shared/action-indicator/indicator.ts new file mode 100644 index 00000000..2f41d2c6 --- /dev/null +++ b/desktop/angular/src/app/shared/action-indicator/indicator.ts @@ -0,0 +1,78 @@ +import { animate, state, style, transition, trigger } from '@angular/animations'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, HostListener, Inject, OnInit } from '@angular/core'; +import { takeUntil } from 'rxjs/operators'; +import { ActionIndicatorRef, ACTION_REF } from './action-indicator.service'; + +@Component({ + templateUrl: './indicator.html', + styleUrls: ['./indicator.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + trigger('slideIn', [ + state('void', style({ + opacity: 0, + transform: 'translateY(32px)' + })), + + state('showing', style({ + opacity: 1, + transform: 'translateY(0px)' + })), + + state('replace', style({ + transform: 'translateY(0px) rotate(-3deg)', + zIndex: -100, + })), + + transition('showing => replace', animate('10ms cubic-bezier(0, 0, 0.2, 1)')), + transition('void => *', animate('220ms cubic-bezier(0, 0, 0.2, 1)')), + + transition('showing => void', animate('220ms cubic-bezier(0, 0, 0.2, 1)', style({ + opacity: 0, + transform: 'translateX(-100%)' + }))), + + transition('replace => void', animate('220ms cubic-bezier(0, 0, 0.2, 1)', style({ + opacity: 0, + transform: 'translateY(-64px) rotate(-3deg)' + }))) + ]) + ] +}) +export class IndicatorComponent implements OnInit { + constructor( + @Inject(ACTION_REF) + public ref: ActionIndicatorRef, + public cdr: ChangeDetectorRef, + ) { } + + @HostBinding('@slideIn') + state = 'showing'; + + @HostBinding('class.error') + isError = this.ref.status === 'error'; + + @HostListener('click') + closeIndicator() { + this.ref.close(); + } + + @HostListener('@slideIn.done', ['$event']) + onAnimationDone() { + if (this.state === 'replace') { + this.ref.close(); + } + } + + ngOnInit() { + this.ref.onCloseReplace + .pipe( + takeUntil(this.ref.onClose), + ) + .subscribe(state => { + this.state = 'replace'; + this.cdr.detectChanges(); + }) + } +} + diff --git a/desktop/angular/src/app/shared/animations.ts b/desktop/angular/src/app/shared/animations.ts new file mode 100644 index 00000000..32989217 --- /dev/null +++ b/desktop/angular/src/app/shared/animations.ts @@ -0,0 +1,111 @@ +import { animate, query, stagger, style, transition, trigger } from '@angular/animations'; + +export const fadeInAnimation = trigger( + 'fadeIn', + [ + transition( + ':enter', + [ + style({ opacity: 0, transform: 'translateY(-5px)' }), + animate('120ms cubic-bezier(0, 0, 0.2, 1)', + style({ opacity: 1, transform: 'translateY(0px)' })) + ] + ), + ] +); + +export const fadeOutAnimation = trigger( + 'fadeOut', + [ + transition( + ':leave', + [ + style({ opacity: 1, transform: 'translateY(0px)' }), + animate('120ms cubic-bezier(0, 0, 0.2, 1)', + style({ opacity: 0, transform: 'translateY(-5px)' })) + ] + ), + ] +); + +export const fadeInListAnimation = trigger( + 'fadeInList', + [ + transition(':enter, * => 0, * => -1', []), + transition(':increment', [ + query(':enter', [ + style({ opacity: 0 }), + stagger(5, [ + animate('300ms ease-out', style({ opacity: 1 })), + ]), + ], { optional: true }) + ]), + ] +) + +export const moveInOutLeftAnimation = trigger( + 'moveInOutLeft', + [ + transition( + ':enter', + [ + style({ opacity: 0, transform: 'translateX(-100%)' }), + animate('.1s ease-in', + style({ opacity: 1, transform: 'translateX(0%)' })) + ] + ), + transition( + ':leave', + [ + style({ opacity: 1, 'z-index': -100 }), + animate('.1s ease-out', + style({ opacity: 0, transform: 'translateX(-100%)' })) + ] + ) + ] +) + + +export const moveInOutAnimation = trigger( + 'moveInOut', + [ + transition( + ':enter', + [ + style({ opacity: 0, transform: 'translateX(100%)' }), + animate('.2s ease-in', + style({ opacity: 1, transform: 'translateX(0%)' })) + ] + ), + transition( + ':leave', + [ + style({ opacity: 1 }), + animate('.2s ease-out', + style({ opacity: 0, transform: 'translateX(100%)' })) + ] + ) + ] +) + +export const moveInOutListAnimation = trigger( + 'moveInOutList', + [ + transition(':enter, * => 0, * => -1', []), + transition(':increment', [ + query(':enter', [ + style({ opacity: 0, transform: 'translateX(100%)' }), + stagger(50, [ + animate('200ms ease-out', style({ opacity: 1, transform: 'translateX(0%)' })), + ]), + ], { optional: true }) + ]), + transition(':decrement', [ + query(':leave', [ + stagger(-50, [ + animate('200ms ease-out', style({ opacity: 0, transform: 'translateX(100%)' })), + ]), + ], { optional: true }) + ]), + ] +) diff --git a/desktop/angular/src/app/shared/app-icon/app-icon-resolver.ts b/desktop/angular/src/app/shared/app-icon/app-icon-resolver.ts new file mode 100644 index 00000000..0a7547ad --- /dev/null +++ b/desktop/angular/src/app/shared/app-icon/app-icon-resolver.ts @@ -0,0 +1,118 @@ +import { Injectable, inject, isDevMode } from "@angular/core"; +import { AppProfile, AppProfileService, deepClone } from "@safing/portmaster-api"; +import { firstValueFrom, map, switchMap } from "rxjs"; +import { INTEGRATION_SERVICE, ProcessInfo } from "src/app/integration"; +import * as parseDataURL from 'data-urls'; + +export abstract class AppIconResolver { + abstract resolveIcon(profile: AppProfile): void; +} + +@Injectable() +export class DefaultIconResolver extends AppIconResolver { + private integration = inject(INTEGRATION_SERVICE); + private profileService = inject(AppProfileService); + + private pendingResolvers = new Map>(); + + resolveIcon(profile: AppProfile): void { + const key = `${profile.Source}/${profile.ID}`; + + // if there's already a promise in flight, abort. + if (this.pendingResolvers.has(key)) { + if (isDevMode()) { + console.log(`[icon:${profile.Name}] loading icon already in progress ...`) + } + + return; + } + + let promise = new Promise((resolve) => { + this.profileService + .getProcessesByProfile(profile) + .pipe( + map(processes => { + // if we there are no running processes for this profile, + // we try to find the icon based on the information stored in + // the profile. + let info: ProcessInfo[] = [{ + execPath: profile.LinkedPath, + cmdline: profile.PresentationPath, + pid: -1, + matchingPath: profile.PresentationPath, + }] + + processes?.forEach(process => { + // BUG: Portmaster sometimes runs a null entry, skip it here. + if (!process) { + return; + } + + // insert at the beginning since the process data might reveal + // better results than the profile one. + info.splice(0, 0, { + execPath: process.Path, + cmdline: process.CmdLine, + pid: process.Pid, + matchingPath: process.MatchingPath, + }) + }) + + return info; + }) + ).subscribe(async (processInfos) => { + for (const info of processInfos) { + try { + await this.loadAndSaveIcon(info, profile); + + // success, abort now + resolve(); + return; + } catch (err) { + // continue using the next one + } + } + + // we failed to find an icon, still resolve the promise here + // because nobody actually cares .... + resolve(); + }) + }); + this.pendingResolvers.set(key, promise); + + promise.finally(() => this.pendingResolvers.delete(key)); + } + + private async loadAndSaveIcon(info: ProcessInfo, profile: AppProfile): Promise { + const icon = await this.integration.getAppIcon(info); + + const dataURL = parseDataURL(icon); + if (!dataURL) { + throw new Error("invalid data url"); + } + const blob = new Blob([dataURL.body], { + type: dataURL.mimeType.essence, + }) + + const body = await blob.arrayBuffer(); + + const save$ = this.profileService + .setProfileIcon(body, blob.type) + .pipe(switchMap(result => { + // save the profile icon + profile = deepClone(profile); + profile.Icons = [ + ...(profile.Icons || []), + { + Value: result.filename, + Type: 'api', + Source: 'ui' + } + ]; + + return this.profileService.saveProfile(profile) + })); + + await firstValueFrom(save$); + } +} diff --git a/desktop/angular/src/app/shared/app-icon/app-icon.html b/desktop/angular/src/app/shared/app-icon/app-icon.html new file mode 100644 index 00000000..bc81164d --- /dev/null +++ b/desktop/angular/src/app/shared/app-icon/app-icon.html @@ -0,0 +1,9 @@ + + {{letter}} + + + + + diff --git a/desktop/angular/src/app/shared/app-icon/app-icon.module.ts b/desktop/angular/src/app/shared/app-icon/app-icon.module.ts new file mode 100644 index 00000000..939cac43 --- /dev/null +++ b/desktop/angular/src/app/shared/app-icon/app-icon.module.ts @@ -0,0 +1,23 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { AppIconComponent } from "./app-icon"; +import { AppIconResolver, DefaultIconResolver } from "./app-icon-resolver"; + +@NgModule({ + imports: [ + CommonModule + ], + declarations: [ + AppIconComponent, + ], + exports: [ + AppIconComponent, + ], + providers: [ + { + provide: AppIconResolver, + useClass: DefaultIconResolver, + } + ] +}) +export class SfngAppIconModule { } diff --git a/desktop/angular/src/app/shared/app-icon/app-icon.scss b/desktop/angular/src/app/shared/app-icon/app-icon.scss new file mode 100644 index 00000000..159cce02 --- /dev/null +++ b/desktop/angular/src/app/shared/app-icon/app-icon.scss @@ -0,0 +1,28 @@ +:host { + border-radius: 50%; + user-select: none; + + height: var(--app-icon-size, 25px); + width: var(--app-icon-size, 25px); + flex-shrink: 0; + @apply mr-2; + + display: inline-flex; + justify-content: center; + align-items: center; +} + +span, +img { + @apply text-primary; + @apply font-medium; + @apply rounded-full; + text-shadow: rgba(0, 0, 0, .8) 0px 0px 1px; + + font-size: calc(var(--app-icon-size, 25px) / 6 * 4); +} + +img { + width: 100%; + height: 100%; +} diff --git a/desktop/angular/src/app/shared/app-icon/app-icon.ts b/desktop/angular/src/app/shared/app-icon/app-icon.ts new file mode 100644 index 00000000..f013f4e8 --- /dev/null +++ b/desktop/angular/src/app/shared/app-icon/app-icon.ts @@ -0,0 +1,312 @@ +import { Min } from './../../../../dist-lib/safing/portmaster-api/lib/netquery.service.d'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + HostBinding, + Inject, + Input, + OnDestroy, + OnInit, + SkipSelf, + inject, +} from '@angular/core'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { + AppProfileService, + PORTMASTER_HTTP_API_ENDPOINT, + PortapiService, + Record, + deepClone, +} from '@safing/portmaster-api'; +import { Subscription, map, of, throwError } from 'rxjs'; +import { catchError, switchMap } from 'rxjs/operators'; +import { INTEGRATION_SERVICE, ProcessInfo } from 'src/app/integration'; +import { AppIconResolver } from './app-icon-resolver'; + +// Interface that must be satisfied for the profile-input +// of app-icon. +export interface IDandName { + // ID of the profile. + ID?: string; + + // Source is the source of the profile. + Source?: string; + + // Name of the profile. + Name: string; +} + +// Some icons we don't want to show on the UI. +// Note that this works on a best effort basis and might +// start breaking with updates to the built-in icons... +const iconsToIngore = [ + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABU0lEQVRYhe2WTUrEQBCF36i4ctm4FsdTKF5AEFxL0knuILgQXAy4ELxDfgTXguAFRG/hDXKCAbtcOB3aSVenMjPRTb5NvdCE97oq3QQYGflnJlbc3T/QXxrfXF9NAGBraKPTk2Nvtey4D1l8OUiIo8ODX/Xt/cMfQCk1SAAi8upWgLquWy8rpbB7+yk2m8+mYvNWAAB4fnlt9MX5WaP397ZhCPgygCFa1IUmwJifCgB5nrMBtdbhAK6pi9QcALIs8+5c1AEOqTmwZge4EUjNiQhpmjbarcvaG4AbgcTcUhSFfwFAHMfhABxScwBIkgRA9wnwBgiOQGBORCjLkl2PoigcgB2BwNzifmi97wEOqTkRoaoqdr2zA9wIJOYWrTW785VPQR+WO2B3vdYIpBBRc9Qkp2Cw/4GVR+BjPpt23u19tUXUgU2aBzuQPz5J8oyMjGyUb9+FOUOmulVPAAAAAElFTkSuQmCC', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAACLElEQVR4nO2av07DMBDGP1DFxtaFmbeg6gtUqtQZtU3yDkgMSAxIDEi8Q/8gMVdC4m1YYO0TMNQspErdOG3Md25c7rc0st3E353v7EsLKIqiKIqiKMq/5MRueHx6NoeYSCjubm82NJ8eaiISdDtX6HauKq9tWsFmF4DPr6+d1zalBshG18RpNYfJy+tW21GFgA+lK6DdboeeBwVjyvO3qx1wGGC5XO71wCYZykc8QEqCZ/cfjNs4+X64rOz3FQ/sMMDi7R2Dfg+Lt/eN9kG/tzX24rwFA8AYYGXM+nr9aQADs9mG37FWW3HsqqBhMpnsFFRGkiTOvkoD5ELLBNtIiLcdmGXZ5jP/4Pkc2i4gIb5KRl3xrnbaQSiEeN8QGI/Hzj5aDgjh+SzLaJ7P4eWAiJZ9EVoIhBA/nU695uYdAnUI4fk0TUvbXeP3gZcDhMS7CLIL1DsHyIv3DYHRaOTs44YAZD2fpik9EfIOQohn2Rch5wBZ8bPZzOObfwiBurWAtOftoqaO511jaSEgJd4FQzwgmAQlxPuGwHA4dPbJ1QICnk+ShOb5HJlaoOHLvgi/FhAUP5/P9xpbteRtyDlA1vN2UVPH8+K7gJR45/MI4gHyK7HYxANsA7BuVvkcnniAXAtIwxYPRPTboIR4IBIDMMSL7wIhYZbF0RmgsS9EQtDY1+L5r7esCUrGvA3xHBCfeIBkgBjEi+0CMYsHHDmg7N9UiqIoiqIoiqIcFT++NKIXgDvowAAAAABJRU5ErkJggg==', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAABqUlEQVRYhe2XP2rDMBSHfymhU0dDD5BbJOQCgUDmEv+7Q6FDoUOgQ6F3cJxC50Agt+nSrD5BBr8OqVyrtfWkl8ShoG+SjJE+/95DwoDH4/nf9NTg+eWVLinym8eH+x4AXF1i8/FoiPFoaBwr+p3bAfjc7dixQhNMw7szatmTvb1XY00wCILOZYjIONcEi6JoXSgIAlw/fYhF9ouBsxzQ0IPrzRaz6QTrzbZ6NptOqvHtTR8EQklAWQIl4WdOQEkEqsaHefm9b5Zl7IfEcWwWVDJ1Ke0rHeXqmaRpeljDIrlWQQ5XufreNglGUWQW5EoslQOAJEm0uagHuRJL5YgIy+Wycc06bIIcEjmFStCUnPGYASxKLJQDYJVgGIZmQZsSS+SAv0eIKblWQQ6pHBEhz3N2fTZBrsQSOYVK0JQc24N2JXaXA2CV4Hw+NwtySOUA/QixvU1kPSiQIyKsViv2vaMTlMgpoihik2N7kEMqB6AxwXpiVlfduSAi7Qix7cGL/DS5XHWdC7rIAY4l3i8GTk1+zLsKpwS7lnMS7ErOeMzU/0c9Ho/nNHwBdUH2gB9vJRsAAAAASUVORK5CYII=', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAByElEQVRYhe1WQUoDQRCsmSh4CAreo3/w4CdE8JirLzCKGhRERPBqfISQx3j0BcaDJxHNRWS7PWRmtmdmJ9mNiSuYOmyYbOiqruoeAizw36G6p0e3WulOHeTE1NO/Qb6zu1f4qZXuqLPuMV9d38xbQyEuL86ha2EWWJKHfr+P4XAIAGg2m2i32wCA7fsXPH9kABjMgHkADP87cW6tNvCwvzG2biRAvpAYvH+54mCAmUcvmI0Yq4nM74DBG02sGwlIgqigS/ZEgdkcrSAuVbpUBEyjTiP7JSkDzKZrdo+xdSMBKas4y4K8befSiVxcLnR83UhACtYBV9TOgbBbOX4TF2YZQZY5Yi9/MYwkXQjy/3EEtjp7LgQzAeOUVSo0zCACcgOnwjUEC2LE7kxApS0AGFRgP4vZ8M5VBaQjoNGKuQ20Q2ney8Gr0H0kIAU7hK4zYiPCJxtFZYRMIyAdAQWrFgyicMSfj4oCkheRmQFyIoq2IRcy9T2QhNmCfN/FVcwMBSWu4XlsQUZe5tZmZW0HBXGU4o4FpCJorS3j6fXTEOVdUrgNApvrK9UFpPB4vlWq2DSo/S+Z6p4c9rRuHNRBTsR3dfAu8LfwDdGgu25Uax8RAAAAAElFTkSuQmCC', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAByUlEQVRYhe1WQUoDQRCs2UTwEBS8R//gwU+I4DFXX2AENRgQEcGr8RFCHuPRFxgPnkQ0F9Ht9rAzsz0zO8luTFzB1GHDZENXdVX3EGCJ/w7VO+3eJKrZrYOc+GuQ/Ab57t5+4Weiml111jvmy6vrRWsoxMV5H0ktzAJNeRgOhxiPxwCAVquFTqcDANi5e8bTewqAwQzoB8BwvxPn9loD9webE+sGAuQLidHbpy0OBpg5e8GsxRhNpH8HjF5pat1AQBREBV2yIwrM+mgEcanSpSJgyjoN7JekDDDrrtk+JtYNBMSs4jT18jadSydycbnQyXUDATEYB2xRMwfCbmX5dVyYZwRpaomd/MUwknTBy//HEZjq7LjgzQS0U0ap0DCHCMgOnPLXECyIEbozBZW2AGBQgf0sZsM5VxUQj4CyFbMbaIZSv5eDV6H7QEAMZghtZ8RahEuWRaWFzCIgHgF5q+YNonDEnY+KAqIXkZ4BsiKKtiEXMvM9EIXegnzfxVXMDAUlruFFbEFKTubGZmVsB3lxlOIOBcQiaK+v4PHlQxPlXZK/DQJbG6vVBcTw0N8uVWwW1P6XTPVOjgZJ0jisg5yIb+vgXeJv4RvrxrtwzfCUqAAAAABJRU5ErkJggg==', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADo0lEQVRYhe2Wu28cVRTGf+fcuzZeY0NCUqTgD3C8mzRU0KDQEgmqoBSGhoKWAlFEKYyQAAkrTRRqOpCQkOhAkUCio4D4IYSAzkkKB+wQbLy7c8+hmNmd2WecDQoFfNJodGfn7vc4Z84M/I9/GfLeB1cutdqH7zxSUli9fOntd4EsmrVXL1xcodVqAf6PEl37+AveWDk/dP78s08vA1eBvSgSDnd3bs49DJGKICIg+dod3J3XXn6Ogz9+49WXnu07F1gA9mOWJRqNBrNRcJ8mAQF8ZHYyuBYhI/DlV9cBAqARnBAj2agdjwARoBaETnK+/eY7NMwfaaPZPueefwaA73+4MfKeM80GAC+8+QkA19cukCQOC+ga1zDPR1//jIgjWhzBEQWNBupoNESdldNn2dm5w/FjT/SIpkEcvLAwX0PUQRwNXQGOBCvXoVpxZ31jc2ICEwWY+1y19AvzEQr3GgAtiLUUo8F690tB5DhC3sgiw800f2p/fAJ/tTtoyMOo1yOqnscdnINOIqNDO+vQbrdwMTRWEnBhfXNyAvOn9qmfOBgvwKxwC9TnAskTN3f32PnzHi1robEbv6HFUVGQJ+AOIvkQgL4U6icOqC9OSKCKu4cH/HT7Nh3P0GiEWkEcc+LBEhylB+qL+ywe+328gGrFNre3kWiE6EjsOi5EqPVS6EGEZrOJW0JVR5KMIy8TqCjQmlUcl7GLlvGrlgLcYWNzY2ICk1CUoFSgtdRPHAwtYteQeimUCuDsmebEMX7l3Pv3E1BCY+lUgqNaFZJ663ID3Fh/6ARKhFrqNVq15lVy1dRP1FjGRaZ6lQwnEKqkw+Si/QLMATwnHxhA7o65k2UJM0NwanOP30dATAPkhmjlmuYiuhCcja0fR7prNhqA4W5Fjwz3ydBTEGLZaKoV99p13y8AnGZjeeT4dfd8LrnnCYyoUQTQQsGtW7/y+tPnR7oZxPb2LywvncRd2dzaGnnP6aUlzBLJvKt1tIAsObUAF195kZ2dO0cSsLx0EgAz6yWQO3aSGeZOJ8swS5gNj+c+AeYwE4QgxlPHF6nNzkBKpGQ4EGMAnSksOGCA41nisJP/eTfuVIjAHQRCCITiPaPjBAC0kwMKMkvW7vuJTgZQffSkOBRCLqeL0cN4PKLA6trah2/FGB97wL05oSohKCEEzMBSRkpp4gf+3d3dq+SOTIAZ4Enyz+QwjYgpkIB7wF6RIxGo8eAJTgsDOpB/jP+38TcKdstukjAxWQAAAABJRU5ErkJggg==', +]; + +const profilesToIgnore = ['local/_unidentified', 'local/_unsolicited']; + +@Component({ + selector: 'app-icon', + templateUrl: './app-icon.html', + styleUrls: ['./app-icon.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AppIconComponent implements OnInit, OnDestroy { + private sub = Subscription.EMPTY; + private initDone = false; + + private resovler = inject(AppIconResolver); + + /** @private The data-URL for the app-icon if available */ + src: SafeUrl | string = ''; + + /** The profile for which to show the app-icon */ + @Input() + set profile(p: IDandName | null | undefined | string) { + if (typeof p === 'string') { + const parts = p.split("/") + p = { + Source: parts[0], + ID: parts[1], + Name: '', + } + } + + if (!!this._profile && !!p && this._profile.ID === p.ID) { + // skip if this is the same profile + return; + } + + this._profile = p || null; + + if (this.initDone) { + this.updateView(); + } + } + get profile(): IDandName | null | undefined { + return this._profile; + } + private _profile: IDandName | null = null; + + /** isIgnoredProfile is set to true if the profile is part of profilesToIgnore */ + isIgnoredProfile = false; + + /** If not icon is available, this holds the first - uppercased - letter of the app - name */ + letter: string = ''; + + /** @private The background color of the component, based on icon availability and generated by ID */ + @HostBinding('style.background-color') + color: string = 'var(--text-tertiary)'; + + constructor( + private profileService: AppProfileService, + private changeDetectorRef: ChangeDetectorRef, + private portapi: PortapiService, + // @HostBinding() is not evaluated in our change-detection run but rather + // checked by the parent component during updateRenderer. + // Since we want the background color to change immediately after we set the + // src path we need to tell the parent (which ever it is) to update as wel. + @SkipSelf() private parentCdr: ChangeDetectorRef, + private sanitzier: DomSanitizer, + @Inject(PORTMASTER_HTTP_API_ENDPOINT) private httpAPI: string + ) { } + + /** Updates the view of the app-icon and tries to find the actual application icon */ + private requestedAnimationFrame: number | null = null; + private updateView(skipIcon = false) { + if (this.requestedAnimationFrame !== null) { + cancelAnimationFrame(this.requestedAnimationFrame); + } + + this.requestedAnimationFrame = requestAnimationFrame(() => { + this.__updateView(); + }) + } + + ngOnInit(): void { + this.updateView(); + this.initDone = true; + } + + private __updateView(skipIcon = false) { + this.requestedAnimationFrame = null; + + const p = this.profile; + const sourceAndId = this.getIDAndSource(); + + if (!!p && sourceAndId !== null) { + let idx = 0; + for (let i = 0; i < (p.ID || p.Name).length; i++) { + idx += (p.ID || p.Name).charCodeAt(i); + } + + const combinedID = `${sourceAndId[0]}/${sourceAndId[1]}`; + this.isIgnoredProfile = profilesToIgnore.includes(combinedID); + + this.updateLetter(p); + + if (!this.isIgnoredProfile) { + this.color = AppColors[idx % AppColors.length]; + } else { + this.color = 'transparent'; + } + + if (!skipIcon) { + this.tryGetSystemIcon(p); + } + + } else { + this.isIgnoredProfile = false; + this.color = 'var(--text-tertiary)'; + } + + this.changeDetectorRef.markForCheck(); + this.parentCdr.markForCheck(); + } + + private updateLetter(p: IDandName) { + if (p.Name !== '') { + if (p.Name[0] === '<') { + // we might get the name with search-highlighting which + // will then include tags. If the first character is a < + // make sure to strip all HTML tags before getting [0]. + this.letter = p.Name.replace( + /( |<([^>]+)>)/gi, + '' + )[0].toLocaleUpperCase(); + } else { + this.letter = p.Name[0]; + } + + this.letter = this.letter.toLocaleUpperCase(); + } else { + this.letter = '?'; + } + } + + getIDAndSource(): [string, string] | null { + if (!this.profile) { + return null; + } + + let id = this.profile.ID; + if (!id) { + return null; + } + + // if there's a source ID only holds the profile ID + if (!!this.profile.Source) { + return [this.profile.Source, id]; + } + + // otherwise, ID likely contains the source + let [source, ...rest] = id.split('/'); + if (rest.length > 0) { + return [source, rest.join('/')]; + } + + // id does not contain a forward-slash so we + // assume the source is local + return ['local', id]; + } + + /** + * Tries to get the application icon form the system. + * Requires the app to be running in the electron wrapper. + */ + private tryGetSystemIcon(p: IDandName) { + const sourceAndId = this.getIDAndSource(); + if (sourceAndId === null) { + return; + } + + this.sub.unsubscribe(); + + this.sub = this.profileService + .watchAppProfile(sourceAndId[0], sourceAndId[1]) + .pipe( + switchMap((profile) => { + this.updateLetter(profile); + + if (!!profile.Icons?.length) { + const firstIcon = profile.Icons[0]; + + console.log(`profile ${profile.Name} has icon of from source ${firstIcon.Source} stored in ${firstIcon.Type}`) + + switch (firstIcon.Type) { + case 'database': + return this.portapi + .get(firstIcon.Value) + .pipe( + map((result) => { + return result.iconData; + }) + ); + + case 'api': + return of(`${this.httpAPI}/v1/profile/icon/${firstIcon.Value}`); + + default: + console.error(`Icon type ${firstIcon.Type} not yet supported`); + } + } + + this.resovler.resolveIcon(profile); + + // return an empty icon here. If the resolver manages to find an icon + // the profle will get updated and we'll run again here. + return of(''); + }) + ) + .subscribe({ + next: (icon) => { + if (iconsToIngore.some((i) => i === icon)) { + icon = ''; + } + if (icon !== '') { + this.src = this.sanitzier.bypassSecurityTrustUrl(icon); + this.color = 'unset'; + } else { + this.src = ''; + this.color = + this.color === 'unset' ? 'var(--text-tertiary)' : this.color; + } + this.changeDetectorRef.detectChanges(); + this.parentCdr.markForCheck(); + }, + error: (err) => console.error(err), + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } +} + +export const AppColors: string[] = [ + 'rgba(244, 67, 54, .7)', + 'rgba(233, 30, 99, .7)', + 'rgba(156, 39, 176, .7)', + 'rgba(103, 58, 183, .7)', + 'rgba(63, 81, 181, .7)', + 'rgba(33, 150, 243, .7)', + 'rgba(3, 169, 244, .7)', + 'rgba(0, 188, 212, .7)', + 'rgba(0, 150, 136, .7)', + 'rgba(76, 175, 80, .7)', + 'rgba(139, 195, 74, .7)', + 'rgba(205, 220, 57, .7)', + 'rgba(255, 235, 59, .7)', + 'rgba(255, 193, 7, .7)', + 'rgba(255, 152, 0, .7)', + 'rgba(255, 87, 34, .7)', + 'rgba(121, 85, 72, .7)', + 'rgba(158, 158, 158, .7)', + 'rgba(96, 125, 139, .7)', +]; diff --git a/desktop/angular/src/app/shared/app-icon/index.ts b/desktop/angular/src/app/shared/app-icon/index.ts new file mode 100644 index 00000000..90675ee7 --- /dev/null +++ b/desktop/angular/src/app/shared/app-icon/index.ts @@ -0,0 +1,2 @@ +export { AppIconComponent } from './app-icon'; +export { SfngAppIconModule } from './app-icon.module'; diff --git a/desktop/angular/src/app/shared/config/basic-setting/basic-setting.html b/desktop/angular/src/app/shared/config/basic-setting/basic-setting.html new file mode 100644 index 00000000..11e864a8 --- /dev/null +++ b/desktop/angular/src/app/shared/config/basic-setting/basic-setting.html @@ -0,0 +1,69 @@ + + + + + + + + + {{opt.Name}} + + + + + + {{opt}} + + + + + + +
+ + + {{ unit }} + +
+
+
+ + +
+ + + {{ unit }} + +
+ + + + + + + + + + +
+
diff --git a/desktop/angular/src/app/shared/config/basic-setting/basic-setting.scss b/desktop/angular/src/app/shared/config/basic-setting/basic-setting.scss new file mode 100644 index 00000000..0bb87370 --- /dev/null +++ b/desktop/angular/src/app/shared/config/basic-setting/basic-setting.scss @@ -0,0 +1,28 @@ +label { + @apply text-sm; +} + +input[type="checkbox"] { + float: right; + user-select: none; +} + +.input-container { + display: block; + position: relative; + font-size: 0.75rem; + + input { + font-size: inherit; + } + + .suffix { + user-select: none; + position: absolute; + left: 0; + top: calc(50% - 0.55rem); + padding-left: 0.3rem; + color: #aaa; + font: inherit; + } +} diff --git a/desktop/angular/src/app/shared/config/basic-setting/basic-setting.ts b/desktop/angular/src/app/shared/config/basic-setting/basic-setting.ts new file mode 100644 index 00000000..82433fb8 --- /dev/null +++ b/desktop/angular/src/app/shared/config/basic-setting/basic-setting.ts @@ -0,0 +1,333 @@ +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { DOCUMENT } from '@angular/common'; +import { AfterViewChecked, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, Inject, Input, Output, ViewChild } from '@angular/core'; +import { AbstractControl, ControlValueAccessor, NgModel, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms'; +import { BaseSetting, ExternalOptionHint, OptionType, parseSupportedValues, SettingValueType, WellKnown } from '@safing/portmaster-api'; + +@Component({ + selector: 'app-basic-setting', + templateUrl: './basic-setting.html', + styleUrls: ['./basic-setting.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: forwardRef(() => BasicSettingComponent), + }, + { + provide: NG_VALIDATORS, + multi: true, + useExisting: forwardRef(() => BasicSettingComponent), + } + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BasicSettingComponent> implements ControlValueAccessor, Validator, AfterViewChecked { + /** @private template-access to all external option hits */ + readonly optionHints = ExternalOptionHint; + + /** @private template-access to parseSupportedValues */ + readonly parseSupportedValues = parseSupportedValues; + + @ViewChild('suffixElement', { static: false, read: ElementRef }) + suffixElement?: ElementRef; + + /** Cached canvas element used by getTextWidth */ + private cachedCanvas?: HTMLCanvasElement; + + /** Returns the value of external-option hint annotation */ + externalOptType(opt: S): ExternalOptionHint | null { + return opt.Annotations?.["safing/portbase:ui:display-hint"] || null; + } + + /** Whether or not the input should be currently disabled. */ + @Input() + set disabled(v: any) { + const disabled = coerceBooleanProperty(v); + this.setDisabledState(disabled); + } + get disabled() { + return this._disabled; + } + + /** The setting to display */ + @Input() + setting: S | null = null; + + /** Emits when the user activates focus on this component */ + @Output() + blured = new EventEmitter(); + + /** @private The ngModel in our view used to display the value */ + @ViewChild(NgModel) + model: NgModel | null = null; + + /** The unit of the setting */ + get unit() { + if (!this.setting) { + return ''; + } + return this.setting.Annotations[WellKnown.Unit] || ''; + } + + /** + * Holds the value as it is presented to the user. + * That is, a JSON encoded object or array is dumped as a + * JSON string. Strings, numbers and booleans are presented + * as they are. + */ + _value: string | number | boolean = ""; + + /** + * Describes the type of the original settings value + * as passed to writeValue(). + * This may be anything that can be returned from `typeof v`. + * If set to "string", "number" or "boolean" then _value is emitted + * as it is. + * If it's set anything else (like "object") than _value is JSON.parse`d + * before being emitted. + */ + _type: string = ''; + + /* Returns true if the current _type and _value is managed as JSON */ + get isJSON(): boolean { + return this._type !== 'string' + && this._type !== 'number' + && this._type !== 'boolean' + } + + /* + * _onChange is set using registerOnChange by @angular/forms + * and satisfies the ControlValueAccessor. + */ + private _onChange: (_: SettingValueType) => void = () => { }; + + /* _onTouch is set using registerOnTouched by @angular/forms + * and satisfies the ControlValueAccessor. + */ + private _onTouch: () => void = () => { }; + + private _onValidatorChange: () => void = () => { }; + + /* Whether or not the input field is disabled. Set by setDisabledState + * from @angular/forms + */ + _disabled: boolean = false; + private _valid: boolean = true; + + // We are using ChangeDetectionStrategy.OnPush so angular does not + // update ourself when writeValue or setDisabledState is called. + // Using the changeDetectorRef we can take care of that ourself. + constructor( + @Inject(DOCUMENT) private document: Document, + private _changeDetectorRef: ChangeDetectorRef + ) { } + + ngAfterViewChecked() { + // update the suffix position everytime angular has + // checked our view for changes. + this.updateUnitSuffixPosition(); + } + + /** + * Sets the user-presented value and emits a change. + * Used by our view. Not meant to be used from outside! + * Use writeValue instead. + * @private + * + * @param value The value to set + */ + setInternalValue(value: string | number | boolean) { + let toEmit: any = value; + try { + if (!this.isJSON) { + toEmit = value; + } else { + toEmit = JSON.parse(value as string); + } + } catch (err) { + this._valid = false; + this._onValidatorChange(); + return; + } + + this._valid = true; + this._value = value; + this._onChange(toEmit); + this.updateUnitSuffixPosition(); + } + + /** + * Updates the position of the value's unit suffix element + */ + private updateUnitSuffixPosition() { + if (!!this.unit && !!this.suffixElement) { + const input = this.suffixElement.nativeElement.previousSibling! as HTMLInputElement; + const style = window.getComputedStyle(input); + let paddingleft = parseInt(style.paddingLeft.slice(0, -2)) + // we need to use `input.value` instead of `value` as we need to + // get preceding zeros of the number input as well, while still + // using the value as a fallback. + let value = input.value || (this._value as string); + const width = this.getTextWidth(value, style.font) + paddingleft; + this.suffixElement.nativeElement.style.left = `${width}px`; + } + } + + /** + * Validates if "value" matches the settings requirements. + * It satisfies the NG_VALIDATORS interface and validates the + * value for THIS component. + * + * @param param0 The AbstractControl to validate + */ + validate({ value }: AbstractControl): ValidationErrors | null { + if (!this._valid) { + return { + jsonParseError: true + } + } + + if (this._type === 'string' || value === null) { + if (!!this.setting?.DefaultValue && !value) { + return { + required: true, + } + } + } + + if (!!this.setting?.ValidationRegex) { + const re = new RegExp(this.setting.ValidationRegex); + + if (!this.isJSON) { + if (!re.test(`${value}`)) { + return { + pattern: `"${value}"` + } + } + } else { + if (!Array.isArray(value)) { + return { + invalidType: true + } + } + const invalidLines = value.filter(v => !re.test(v)); + if (invalidLines.length) { + return { + pattern: invalidLines + } + } + } + } + + return null; + } + + /** + * Writes a new value and satisfies the ControlValueAccessor + * + * @param v The new value to write + */ + writeValue(v: SettingValueType) { + // the following is a super ugly work-around for the migration + // from security-settings to booleans. + // + // In order to not mess and hide an actual portmaster issue + // we only convert v to a boolean if it's a number value and marked as a security setting. + // In all other cases we don't mangle it. + // + // TODO(ppacher): Remove in v1.8? + // BOM + if (this.setting?.OptType === OptionType.Bool && this.setting?.Annotations[WellKnown.DisplayHint] === ExternalOptionHint.SecurityLevel) { + if (typeof v === 'number') { + (v as any) = v === 7; + } + } + // EOM + + let t = typeof v; + this._type = t; + + if (this.isJSON) { + this._value = JSON.stringify(v, undefined, 2); + } else { + this._value = v; + } + + this.updateUnitSuffixPosition(); + this._changeDetectorRef.markForCheck(); + } + + registerOnValidatorChange(fn: () => void) { + this._onValidatorChange = fn; + } + + /** + * Registers the onChange function requred by the + * ControlValueAccessor + * + * @param fn The fn to register + */ + registerOnChange(fn: (_: SettingValueType) => void) { + this._onChange = fn; + } + + /** + * @private + * Called when the input-component used for the setting is touched/focused. + */ + touched() { + this._onTouch(); + this.blured.next(); + } + + /** + * Registers the onTouch function requred by the + * ControlValueAccessor + * + * @param fn The fn to register + */ + registerOnTouched(fn: () => void) { + this._onTouch = fn; + } + + /** + * Enable or disable the component. Required for the + * ControlValueAccessor. + * + * @param disable Whether or not the component is disabled + */ + setDisabledState(disable: boolean) { + this._disabled = disable; + this._changeDetectorRef.markForCheck(); + } + + /** + * @private + * Returns the number of lines in value. If value is not + * a string 1 is returned. + */ + lineCount(value: string | number | boolean) { + if (typeof value === 'string') { + return value.split('\n').length + } + return 1 + } + + /** + * Calculates the amount of pixel a text requires when being rendered. + * It uses canvas.measureText on a dummy (no attached) element + * + * @param text The text that would be rendered + * @param font The CSS font descriptor that would be used for the text + */ + private getTextWidth(text: string, font: string): number { + let canvas = this.cachedCanvas || this.document.createElement('canvas'); + this.cachedCanvas = canvas; + + let context = canvas.getContext("2d")!; + context.font = font; + let metrics = context.measureText(text); + return metrics.width; + } +} diff --git a/desktop/angular/src/app/shared/config/basic-setting/index.ts b/desktop/angular/src/app/shared/config/basic-setting/index.ts new file mode 100644 index 00000000..ec1ff492 --- /dev/null +++ b/desktop/angular/src/app/shared/config/basic-setting/index.ts @@ -0,0 +1 @@ +export * from './basic-setting'; diff --git a/desktop/angular/src/app/shared/config/config-settings.html b/desktop/angular/src/app/shared/config/config-settings.html new file mode 100644 index 00000000..f6c9253e --- /dev/null +++ b/desktop/angular/src/app/shared/config/config-settings.html @@ -0,0 +1,111 @@ + + + +
+ + +
+ + + + + +

+ {{subsys.Name}} +

+ + +
+ +
+

{{cat.name}}

+ + + + + +
+ +
+ +
+ + + + +
+
+
+
+
+
+
+ + +

+ Other +

+
+ + + + +
+
+
+
+
diff --git a/desktop/angular/src/app/shared/config/config-settings.scss b/desktop/angular/src/app/shared/config/config-settings.scss new file mode 100644 index 00000000..f839c341 --- /dev/null +++ b/desktop/angular/src/app/shared/config/config-settings.scss @@ -0,0 +1,95 @@ +:host { + display: flex; + overflow: hidden; +} + + +fa-icon[icon="spinner"] { + @apply text-3xl; + display: block; + width: 100%; + text-align: center; + height: 6rem; +} + +div.settings-nav { + @apply mt-4; + flex-shrink: 0; + overflow: visible; + white-space: nowrap; + + transition: height cubic-bezier(0.25, 0.46, 0.45, 0.94) .5s; + @apply text-xs; + + + ul { + position: fixed; + + li { + @apply font-medium; + + &.separated { + margin-top: 1.25rem; + } + + } + + &>li { + @apply mb-1; + @apply text-tertiary; + + span { + cursor: pointer; + display: block; + } + + &:hover, + &.active { + @apply text-primary; + } + + &.active { + &.category:before { + content: ""; + width: 1px; + height: 1rem; + @apply bg-white block absolute; + left: 0.5rem; + } + + ul.settings { + display: inline-block; + } + } + + ul.settings { + position: unset; + @apply mt-2; + @apply ml-2; + @apply pl-3; + @apply text-xs; + @apply border-l; + @apply border-cards-tertiary; + display: none; + + li { + cursor: pointer; + margin-top: 0; + } + } + } + } +} + +.user-defined-value:before { + content: ""; + height: 1rem; + @apply bg-blue block absolute rounded-full w-1 h-1; + top: 0.45rem; + left: -1rem; +} + +.user-defined-value.category:before { + left: -2rem; + top: 0.35rem; +} diff --git a/desktop/angular/src/app/shared/config/config-settings.ts b/desktop/angular/src/app/shared/config/config-settings.ts new file mode 100644 index 00000000..49301abf --- /dev/null +++ b/desktop/angular/src/app/shared/config/config-settings.ts @@ -0,0 +1,606 @@ +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ScrollDispatcher } from '@angular/cdk/overlay'; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + QueryList, + TrackByFunction, + ViewChildren, +} from '@angular/core'; +import { + ConfigService, + ExpertiseLevelNumber, + PortapiService, + Setting, + StringSetting, + releaseLevelFromName, +} from '@safing/portmaster-api'; +import { BehaviorSubject, Subscription, combineLatest } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; +import { StatusService, Subsystem } from 'src/app/services'; +import { + fadeInAnimation, + fadeInListAnimation, + fadeOutAnimation, +} from 'src/app/shared/animations'; +import { FuzzySearchService } from 'src/app/shared/fuzzySearch'; +import { ExpertiseLevelOverwrite } from '../expertise/expertise-directive'; +import { SaveSettingEvent } from './generic-setting/generic-setting'; +import { ActionIndicatorService } from '../action-indicator'; +import { SfngDialogService } from '@safing/ui'; +import { + ExportConfig, + ExportDialogComponent, +} from './export-dialog/export-dialog.component'; +import { + ImportConfig, + ImportDialogComponent, +} from './import-dialog/import-dialog.component'; + +interface Category { + name: string; + settings: Setting[]; + minimumExpertise: ExpertiseLevelNumber; + collapsed: boolean; + hasUserDefinedValues: boolean; +} + +interface SubsystemWithExpertise extends Subsystem { + minimumExpertise: ExpertiseLevelNumber; + isDisabled: boolean; + hasUserDefinedValues: boolean; +} + +@Component({ + selector: 'app-settings-view', + templateUrl: './config-settings.html', + styleUrls: ['./config-settings.scss'], + animations: [fadeInAnimation, fadeOutAnimation, fadeInListAnimation], +}) +export class ConfigSettingsViewComponent + implements OnInit, OnDestroy, AfterViewInit { + subsystems: SubsystemWithExpertise[] = []; + others: Setting[] | null = null; + settings: Map = new Map(); + + /** A list of all selected settings for export */ + selectedSettings: { [key: string]: boolean } = {}; + + /** Whether or not we are currently in "export" mode */ + exportMode = false; + + activeSection = ''; + activeCategory = ''; + loading = true; + + @Input() + resetLabelText = 'Reset to system default'; + + @Input() + set compactView(v: any) { + this._compactView = coerceBooleanProperty(v); + } + get compactView() { + return this._compactView; + } + private _compactView = false; + + @Input() + set lockDefaults(v: any) { + this._lockDefaults = coerceBooleanProperty(v); + } + get lockDefaults() { + return this._lockDefaults; + } + private _lockDefaults = false; + + @Input() + set userSettingsMarker(v: any) { + this._userSettingsMarker = coerceBooleanProperty(v); + } + get userSettingsMarker() { + return this._userSettingsMarker; + } + private _userSettingsMarker = true; + + @Input() + set searchTerm(v: string) { + this.onSearch.next(v); + } + + @Input() + set availableSettings(v: Setting[]) { + this.onSettingsChange.next(v); + } + + @Input() + set scope(scope: 'global' | string) { + this._scope = scope; + } + get scope() { + return this._scope; + } + private _scope: 'global' | string = 'global'; + + @Input() + displayStackable: string | boolean = false; + + @Input() + set highlightKey(key: string | null) { + this._highlightKey = key || null; + this._scrolledToHighlighted = false; + // If we already loaded the settings then instruct the window + // to scroll the setting into the view. + if (!!key && !!this.settings && this.settings.size > 0) { + this.scrollTo(key); + this._scrolledToHighlighted = true; + } + } + get highlightKey() { + return this._highlightKey; + } + private _highlightKey: string | null = null; + private _scrolledToHighlighted = false; + + mustShowSetting: ExpertiseLevelOverwrite = ( + lvl: ExpertiseLevelNumber, + s: Setting + ) => { + if (lvl >= s.ExpertiseLevel) { + // this setting is shown anyway. + return false; + } + if (s.Key === this.highlightKey) { + return true; + } + // the user is searching for settings so make sure we even show advanced or developer settings + if (this.onSearch.getValue() !== '') { + return true; + } + if (s.Value === undefined) { + // no value set + return false; + } + return true; + }; + + mustShowCategory: ExpertiseLevelOverwrite = ( + lvl: ExpertiseLevelNumber, + cat: Category + ) => { + return cat.settings.some((setting) => this.mustShowSetting(lvl, setting)); + }; + + mustShowSubsystem: ExpertiseLevelOverwrite = ( + lvl: ExpertiseLevelNumber, + subsys: SubsystemWithExpertise + ) => { + return !!this.settings + .get(subsys.ConfigKeySpace) + ?.some((cat) => this.mustShowCategory(lvl, cat)); + }; + + @Output() + save = new EventEmitter(); + + private onSearch = new BehaviorSubject(''); + private onSettingsChange = new BehaviorSubject([]); + + @ViewChildren('navLink', { read: ElementRef }) + navLinks: QueryList | null = null; + + private subscription = Subscription.EMPTY; + + constructor( + public statusService: StatusService, + public configService: ConfigService, + private elementRef: ElementRef, + private changeDetectorRef: ChangeDetectorRef, + private scrollDispatcher: ScrollDispatcher, + private searchService: FuzzySearchService, + private actionIndicator: ActionIndicatorService, + private portapi: PortapiService, + private dialog: SfngDialogService + ) { } + + openImportDialog() { + const importConfig: ImportConfig = { + type: 'setting', + key: this.scope, + }; + this.dialog.create(ImportDialogComponent, { + data: importConfig, + autoclose: false, + backdrop: 'light', + }); + } + + toggleExportMode() { + this.exportMode = !this.exportMode; + + if (this.exportMode) { + this.actionIndicator.info( + 'Settings Export', + 'Please select all settings you want to export and press "Save" to generate the export. Note that settings with system defaults cannot be exported and are hidden.' + ); + } + } + + generateExport() { + let selectedKeys = Object.keys(this.selectedSettings).reduce((sum, key) => { + if (this.selectedSettings[key]) { + sum.push(key); + } + + return sum; + }, [] as string[]); + + if (selectedKeys.length === 0) { + selectedKeys = Array.from(this.settings.values()).reduce( + (sum, current) => { + current.forEach((cat) => { + cat.settings.forEach((s) => { + if (s.Value !== undefined) { + sum.push(s.Key); + } + }); + }); + + return sum; + }, + [] as string[] + ); + } + + this.portapi.exportSettings(selectedKeys, this.scope).subscribe({ + next: (exportBlob) => { + const exportConfig: ExportConfig = { + type: 'setting', + content: exportBlob, + }; + + this.dialog.create(ExportDialogComponent, { + data: exportConfig, + backdrop: 'light', + autoclose: true, + }); + + this.exportMode = false; + }, + error: (err) => { + const msg = this.actionIndicator.getErrorMessgae(err); + this.actionIndicator.error('Failed To Generate Export', msg); + }, + }); + } + + saveSetting(event: SaveSettingEvent, s: Setting) { + this.save.next(event); + const subsys = this.subsystems.find( + (subsys) => s.Key === subsys.ToggleOptionKey + ); + if (!!subsys) { + // trigger a reload of the page as we now might need to show more + // settings. + this.onSettingsChange.next(this.onSettingsChange.getValue()); + } + } + + trackSubsystem: TrackByFunction = + this.statusService.trackSubsystem; + + trackCategory(_: number, cat: Category) { + return cat.name; + } + + ngOnInit(): void { + this.subscription = combineLatest([ + this.onSettingsChange, + this.statusService.querySubsystem(), + this.onSearch.pipe(debounceTime(250)), + this.configService.watch('core/releaseLevel'), + ]) + .pipe(debounceTime(10)) + .subscribe( + ([settings, subsystems, searchTerm, currentReleaseLevelSetting]) => { + this.subsystems = subsystems.map((s) => ({ + ...s, + // we start with developer and decrease to the lowest number required + // while grouping the settings. + minimumExpertise: ExpertiseLevelNumber.developer, + isDisabled: false, + hasUserDefinedValues: false, + })); + this.others = []; + this.settings = new Map(); + + // Get the current release level as a number (fallback to 'stable' is something goes wrong) + const currentReleaseLevel = releaseLevelFromName( + currentReleaseLevelSetting || ('stable' as any) + ); + + // Make sure we only display settings that are allowed by the releaselevel setting. + settings = settings.filter( + (setting) => setting.ReleaseLevel <= currentReleaseLevel + ); + + // Use fuzzy-search to limit the number of settings shown. + const filtered = this.searchService.searchList(settings, searchTerm, { + ignoreLocation: true, + ignoreFieldNorm: true, + threshold: 0.1, + minMatchCharLength: 3, + keys: [ + { name: 'Name', weight: 3 }, + { name: 'Description', weight: 2 }, + ], + }); + + // The search service wraps the items in a search-result object. + // Unwrap them now. + settings = filtered.map((res) => res.item); + + // use order-annotations to sort the settings. This affects the order of + // the categories as well as the settings inside the categories. + settings.sort((a, b) => { + const orderA = a.Annotations?.['safing/portbase:ui:order'] || 0; + const orderB = b.Annotations?.['safing/portbase:ui:order'] || 0; + return orderA - orderB; + }); + + settings.forEach((setting) => { + let pushed = false; + this.subsystems.forEach((subsys) => { + if ( + setting.Key.startsWith( + subsys.ConfigKeySpace.slice('config:'.length) + ) + ) { + // get the category name annotation and fallback to 'others' + let catName = 'other'; + if ( + !!setting.Annotations && + !!setting.Annotations['safing/portbase:ui:category'] + ) { + catName = setting.Annotations['safing/portbase:ui:category']; + } + + // ensure we have a category array for the subsystem. + let categories = this.settings.get(subsys.ConfigKeySpace); + if (!categories) { + categories = []; + this.settings.set(subsys.ConfigKeySpace, categories); + } + + // find or create the appropriate category object. + let cat = categories.find((c) => c.name === catName); + if (!cat) { + cat = { + name: catName, + minimumExpertise: ExpertiseLevelNumber.developer, + settings: [], + collapsed: false, + hasUserDefinedValues: false, + }; + categories.push(cat); + } + + // add the setting to the category object and update + // the minimum expertise required for the category. + cat.settings.push(setting); + if (setting.ExpertiseLevel < cat.minimumExpertise) { + cat.minimumExpertise = setting.ExpertiseLevel; + } + + pushed = true; + } + }); + + // if we did not push the setting to some subsystem + // we need to push it to "others" + if (!pushed) { + this.others!.push(setting); + } + }); + + if (this.others.length === 0) { + this.others = null; + } + + // Reduce the subsystem array to only contain subsystems that + // actually have settings to show. + // Also update the minimumExpertiseLevel for those subsystems + this.subsystems = this.subsystems + .filter((subsys) => { + return !!this.settings.get(subsys.ConfigKeySpace); + }) + .map((subsys) => { + let categories = this.settings.get(subsys.ConfigKeySpace)!; + let hasUserDefinedValues = false; + categories.forEach((c) => { + c.hasUserDefinedValues = c.settings.some( + (s) => s.Value !== undefined + ); + hasUserDefinedValues = + c.hasUserDefinedValues || hasUserDefinedValues; + }); + + subsys.hasUserDefinedValues = hasUserDefinedValues; + + let toggleOption: Setting | undefined = undefined; + for (let c of categories) { + toggleOption = c.settings.find( + (s) => s.Key === subsys.ToggleOptionKey + ); + if (!!toggleOption) { + if ( + (toggleOption.Value !== undefined && !toggleOption.Value) || + (toggleOption.Value === undefined && + !toggleOption.DefaultValue) + ) { + subsys.isDisabled = true; + + // remove all settings for all subsystem categories + // except for the ToggleOption. + categories = categories + .map((c) => ({ + ...c, + settings: c.settings.filter( + (s) => s.Key === toggleOption!.Key + ), + })) + .filter((cat) => cat.settings.length > 0); + this.settings.set(subsys.ConfigKeySpace, categories); + } + break; + } + } + + // reduce the categories to find the smallest expertise level requirement. + subsys.minimumExpertise = categories.reduce((min, current) => { + if (current.minimumExpertise < min) { + return current.minimumExpertise; + } + return min; + }, ExpertiseLevelNumber.developer as ExpertiseLevelNumber); + + return subsys; + }); + + // Force the core subsystem to the end. + if (this.subsystems.length >= 2 && this.subsystems[0].ID === 'core') { + this.subsystems.push( + this.subsystems.shift() as SubsystemWithExpertise + ); + } + + // Notify the user interface that we're done loading + // the settings. + this.loading = false; + + // If there's a highlightKey set and we have not yet scrolled + // to it (because it was set during component bootstrap) we + // need to scroll there now. + if (this._highlightKey !== null && !this._scrolledToHighlighted) { + this._scrolledToHighlighted = true; + + // Use the next animation frame for scrolling + window.requestAnimationFrame(() => { + this.scrollTo(this._highlightKey || ''); + }); + } + } + ); + } + + ngAfterViewInit() { + this.subscription = new Subscription(); + + // Whenever our scroll-container is scrolled we might + // need to update which setting is currently highlighted + // in the settings-navigation. + this.subscription.add( + this.scrollDispatcher + .scrolled(10) + .subscribe(() => this.intersectionCallback()) + ); + + // Also, entries in the settings-navigation might become + // visible with expertise/release level changes so make + // sure to recalculate the current one whenever a change + // happens. + this.subscription.add( + this.navLinks?.changes.subscribe(() => { + this.intersectionCallback(); + this.changeDetectorRef.detectChanges(); + }) + ); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + this.onSearch.complete(); + } + + /** + * Calculates which navigation entry should be highlighted + * depending on the scroll position. + */ + private intersectionCallback() { + // search our parents for the element that's scrollable + let elem: HTMLElement = this.elementRef.nativeElement; + while (!!elem) { + if (elem.scrollTop > 0) { + break; + } + elem = elem.parentElement!; + } + + // if there's no scrolled/scrollable parent element + // our content itself is scrollable so use our own + // host element as the anchor for the calculation. + if (!elem) { + elem = this.elementRef.nativeElement; + } + + // get the elements offset to page-top + var offsetTop = 0; + if (!!elem) { + const viewRect = elem.getBoundingClientRect(); + offsetTop = viewRect.top; + } + + this.navLinks?.some((link) => { + const subsystem = link.nativeElement.getAttribute('subsystem'); + const category = link.nativeElement.getAttribute('category'); + + const lastChild = (link.nativeElement as HTMLElement) + .lastElementChild as HTMLElement; + if (!lastChild) { + return false; + } + + const rect = lastChild.getBoundingClientRect(); + const styleBox = getComputedStyle(lastChild); + + const offset = + rect.top + + rect.height - + parseInt(styleBox.marginBottom) - + parseInt(styleBox.paddingBottom); + + if (offset >= offsetTop) { + this.activeSection = subsystem; + this.activeCategory = category; + return true; + } + + return false; + }); + this.changeDetectorRef.detectChanges(); + } + + /** + * @private + * Performs a smooth-scroll to the given anchor element ID. + * + * @param id The ID of the anchor element to scroll to. + */ + scrollTo(id: string, cat?: Category) { + if (!!cat) { + cat.collapsed = false; + } + document.getElementById(id)?.scrollIntoView({ + behavior: 'smooth', + block: 'start', + inline: 'nearest', + }); + } +} diff --git a/desktop/angular/src/app/shared/config/config.module.ts b/desktop/angular/src/app/shared/config/config.module.ts new file mode 100644 index 00000000..127032af --- /dev/null +++ b/desktop/angular/src/app/shared/config/config.module.ts @@ -0,0 +1,77 @@ +import { DragDropModule } from '@angular/cdk/drag-drop'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { + SfngSelectModule, + SfngTipUpModule, + SfngToggleSwitchModule, + SfngTooltipModule, +} from '@safing/ui'; +import { MarkdownModule } from 'ngx-markdown'; +import { ExpertiseModule } from '../expertise/expertise.module'; +import { SfngFocusModule } from '../focus'; +import { SfngMenuModule } from '../menu'; +import { SfngMultiSwitchModule } from '../multi-switch'; +import { BasicSettingComponent } from './basic-setting/basic-setting'; +import { ConfigSettingsViewComponent } from './config-settings'; +import { FilterListComponent } from './filter-lists'; +import { GenericSettingComponent } from './generic-setting'; +import { + OrderedListComponent, + OrderedListItemComponent, +} from './ordererd-list'; +import { RuleListItemComponent } from './rule-list/list-item'; +import { RuleListComponent } from './rule-list/rule-list'; +import { SafePipe } from './safe.pipe'; +import { ExportDialogComponent } from './export-dialog/export-dialog.component'; +import { ImportDialogComponent } from './import-dialog/import-dialog.component'; +import { SfngAppIconModule } from '../app-icon'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + DragDropModule, + SfngTooltipModule, + SfngSelectModule, + SfngMultiSwitchModule, + SfngFocusModule, + SfngMenuModule, + SfngTipUpModule, + FontAwesomeModule, + MarkdownModule, + RouterModule, + ExpertiseModule, + SfngToggleSwitchModule, + MarkdownModule, + SfngAppIconModule + ], + declarations: [ + BasicSettingComponent, + FilterListComponent, + OrderedListComponent, + OrderedListItemComponent, + RuleListComponent, + RuleListItemComponent, + ConfigSettingsViewComponent, + GenericSettingComponent, + SafePipe, + ExportDialogComponent, + ImportDialogComponent, + ], + exports: [ + BasicSettingComponent, + FilterListComponent, + OrderedListComponent, + OrderedListItemComponent, + RuleListComponent, + RuleListItemComponent, + ConfigSettingsViewComponent, + GenericSettingComponent, + SafePipe, + ], +}) +export class ConfigModule { } diff --git a/desktop/angular/src/app/shared/config/export-dialog/export-dialog.component.html b/desktop/angular/src/app/shared/config/export-dialog/export-dialog.component.html new file mode 100644 index 00000000..da8a3cb1 --- /dev/null +++ b/desktop/angular/src/app/shared/config/export-dialog/export-dialog.component.html @@ -0,0 +1,19 @@ +
+

+ {{ dialogRef.data.type === "setting" ? "Settings" : "Profile" }} Export +

+ + +
+ + + +
+ + +
\ No newline at end of file diff --git a/desktop/angular/src/app/shared/config/export-dialog/export-dialog.component.ts b/desktop/angular/src/app/shared/config/export-dialog/export-dialog.component.ts new file mode 100644 index 00000000..f451732e --- /dev/null +++ b/desktop/angular/src/app/shared/config/export-dialog/export-dialog.component.ts @@ -0,0 +1,67 @@ +import { DOCUMENT } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + ElementRef, + OnInit, + inject, +} from '@angular/core'; +import { SFNG_DIALOG_REF, SfngDialogRef } from '@safing/ui'; +import { ActionIndicatorService } from '../../action-indicator'; +import { INTEGRATION_SERVICE } from 'src/app/integration'; + +export interface ExportConfig { + content: string; + type: 'setting' | 'profile'; +} + +@Component({ + templateUrl: './export-dialog.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styles: [ + ` + :host { + @apply flex flex-col gap-2 overflow-hidden; + min-height: 24rem; + min-width: 24rem; + max-height: 40rem; + max-width: 40rem; + } + `, + ], +}) +export class ExportDialogComponent implements OnInit { + readonly dialogRef: SfngDialogRef< + ExportDialogComponent, + unknown, + ExportConfig + > = inject(SFNG_DIALOG_REF); + + private readonly elementRef: ElementRef = inject(ElementRef); + private readonly document = inject(DOCUMENT); + private readonly uai = inject(ActionIndicatorService); + private readonly integration = inject(INTEGRATION_SERVICE); + + content = ''; + + ngOnInit(): void { + this.content = '```yaml\n' + this.dialogRef.data.content + '\n```'; + } + + download() { + const blob = new Blob([this.dialogRef.data.content], { type: 'text/yaml' }); + + const elem = this.document.createElement('a'); + elem.href = window.URL.createObjectURL(blob); + elem.download = 'export.yaml'; + this.elementRef.nativeElement.appendChild(elem); + elem.click(); + this.elementRef.nativeElement.removeChild(elem); + } + + copyToClipboard() { + this.integration.writeToClipboard(this.dialogRef.data.content) + .then(() => this.uai.success('Copied to Clipboard')) + .catch(() => this.uai.error('Failed to Copy to Clipboard')); + } +} diff --git a/desktop/angular/src/app/shared/config/filter-lists/filter-list.html b/desktop/angular/src/app/shared/config/filter-lists/filter-list.html new file mode 100644 index 00000000..a6a72a87 --- /dev/null +++ b/desktop/angular/src/app/shared/config/filter-lists/filter-list.html @@ -0,0 +1,55 @@ +
+ + +
+
+ + + + + + + {{ !!node.license ? 'License: ' + node.license : '' }} + + + + + + +
+ +
+
+ + + Expand + + + + Collapse + +
+
+ +
+
+ + + +
+
+
+ + + + + +
diff --git a/desktop/angular/src/app/shared/config/filter-lists/filter-list.scss b/desktop/angular/src/app/shared/config/filter-lists/filter-list.scss new file mode 100644 index 00000000..1b41d179 --- /dev/null +++ b/desktop/angular/src/app/shared/config/filter-lists/filter-list.scss @@ -0,0 +1,101 @@ +:host { + display: block; + overflow: hidden; + + @apply bg-cards-secondary; + @apply rounded; + @apply p-2; + @apply h-full; +} + +.node { + position: relative; + display: flex; + flex-direction: column; + + justify-content: flex-start; + @apply py-1; + + .head { + display: flex; + flex-direction: row; + align-items: baseline; + + input { + @apply mr-2; + position: relative; + top: 2px; + } + + label { + display: flex; + flex-direction: column; + flex-grow: 1; + } + + span.details { + opacity: 0; + text-transform: capitalize; + font-size: 0.9em; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + max-width: 6rem; + @apply text-tertiary; + } + + &:hover { + span.details { + opacity: 1; + + } + } + } + + span.name { + @apply text-primary; + + .id { + @apply text-tertiary; + font-style: italic; + } + } + + .description { + position: relative; + top: -2px; + + @apply text-tertiary; + } + + div.expand { + cursor: pointer; + @apply text-secondary; + display: flex; + flex-direction: row; + align-items: center; + @apply pb-2; + + fa-icon { + margin-right: 0.25rem; + } + } + + .children { + display: flex; + flex-direction: column; + margin-left: 1.25rem; + } + + .border { + position: absolute; + top: 1.2rem; + bottom: 0.5rem; + width: 0.7rem; + margin-left: -0.85rem; + border: 1px solid; + border-right: none; + border-top: none; + @apply border-cards-tertiary; + } +} diff --git a/desktop/angular/src/app/shared/config/filter-lists/filter-list.ts b/desktop/angular/src/app/shared/config/filter-lists/filter-list.ts new file mode 100644 index 00000000..b39c45b4 --- /dev/null +++ b/desktop/angular/src/app/shared/config/filter-lists/filter-list.ts @@ -0,0 +1,293 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, HostListener, OnDestroy, OnInit } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { PortapiService, Record } from '@safing/portmaster-api'; +import { Subscription } from 'rxjs'; +import { moveInOutListAnimation } from '../../animations'; + +interface Category { + name: string; + id: string; + description: string; + parent?: string | null; +} + +interface Source { + name: string; + id: string; + description: string; + category: string; + // urls: Resource[]; // we don't care about the actual URLs here. + website: string; + contribute: string; + license: string; +} + +interface FilterListIndex extends Record { + version: string; + schemaVersion: string; + categories: Category[]; + sources: Source[]; +} + +interface TreeNode { + id: string; + name: string; + description: string; + children: TreeNode[]; + expanded: boolean; + selected: boolean; + parent?: TreeNode; + website?: string; + license?: string; + hasSelectedChildren: boolean; +} + +@Component({ + selector: 'app-filter-list', + templateUrl: './filter-list.html', + styleUrls: ['./filter-list.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterListComponent), + multi: true, + } + ], + animations: [ + moveInOutListAnimation, + ] +}) +export class FilterListComponent implements OnInit, OnDestroy, ControlValueAccessor { + /** The actual filter-list index as loaded from the portmaster. */ + private index: FilterListIndex | null = null; + + /** @private a list of "tree-nodes" to render */ + nodes: TreeNode[] = []; + + /** A lookup map for fast ID to TreeNode lookups */ + private lookupMap: Map = new Map(); + + /** @private forward blur events to the onTouch callback. */ + @HostListener('blur') + onBlur() { + this.onTouch(); + } + + /** The currently selected IDs. */ + private selectedIDs: string[] = []; + + /** Subscription to watch the filterlist index. */ + private watchSubscription = Subscription.EMPTY; + + constructor(private portapi: PortapiService, + private changeDetectorRef: ChangeDetectorRef) { } + + ngOnInit() { + this.watchSubscription = + this.portapi.watch("cache:intel/filterlists/index") + .subscribe( + index => this.updateIndex(index), + err => { + // Filter list index not yet loaded. + console.error(`failed to get fitlerlist index`, err); + } + ); + } + + ngOnDestroy() { + this.watchSubscription.unsubscribe(); + } + + /** The onChange callback registered by ngModel or form controls */ + private _onChange: (v: string[]) => void = () => { }; + + /** Registers the onChange callback required by ControlValueAccessor */ + registerOnChange(fn: (v: string[]) => void) { + this._onChange = fn; + } + + /** The _onTouch callback registered by ngModel and form controls */ + private onTouch: () => void = () => { }; + + /** Registeres the onTouch callback required by ControlValueAccessor. */ + registerOnTouched(fn: () => void) { + this.onTouch = fn; + } + + /** + * Update the currently selected IDs. Used by ngModel + * and form controls. Implements ControlValueAccessor. + * + * @param ids A list of selected IDs + */ + writeValue(ids: string[]) { + this.selectedIDs = ids; + if (!!this.index) { + this.updateIndex(this.index); + } + } + + /** + * + * @param index The filter list index. + */ + private updateIndex(index: FilterListIndex) { + this.index = index; + + var nodes: TreeNode[] = []; + let lm = new Map(); + let childCategories: Category[] = []; + + // Create a tree-node for each category + this.index.categories.forEach(category => { + let tn: TreeNode = { + id: category.id, + description: category.description, + name: category.name, + children: [], + expanded: this.lookupMap.get(category.id)?.expanded || false, // keep it expanded if the user did not change anything. + selected: false, + hasSelectedChildren: false, + }; + + lm.set(category.id, tn) + + // if the category does not have a parent + // it's a root node. + if (!category.parent) { + nodes.push(tn); + } else { + // we need to handle child-categories later. + childCategories.push(category); + } + }); + + // iterate over all "child" categories and add + // them to the correct parent (which must be in lm already.) + childCategories.forEach(category => { + const tn = lm.get(category.id)!; + const parent = lm.get(category.parent!); + // if the parent category does not exist ignore it + if (!parent) { + return; + } + + parent.children.push(tn); + tn.parent = parent; + }); + + this.index.sources.forEach(source => { + let category = lm.get(source.category); + if (!category) { + return; + } + + let tn: TreeNode = { + id: source.id, + name: source.name, + description: source.description, + children: [], + expanded: false, + selected: false, + parent: category, + website: source.website, + license: source.license, + hasSelectedChildren: false + } + + // Add the source to the lookup-map + lm.set(source.id, tn); + + category.children.push(tn); + }); + + // make sure we expand all parent categories for + // all selected IDs so they are actually visible. + this.selectedIDs.forEach(id => { + const tn = lm.get(id); + if (!tn) { + return; + } + + this.updateNode(tn, true, true, true, false); + + let parent = tn.parent; + while (!!parent) { + parent.expanded = true; + parent.hasSelectedChildren = true; + parent = parent.parent; + } + }); + + this.nodes = nodes; + this.lookupMap = lm; + + this.changeDetectorRef.markForCheck(); + } + + /** Returns all actually selected IDs. */ + private getIDs() { + let ids: string[] = []; + + let collectIds = (n: TreeNode) => { + if (n.selected) { + // If the parent is selected we can ignore the + // childs because they must be selected as well. + ids.push(n.id); + return; + } + + n.children.forEach(child => collectIds(child)); + } + + this.nodes.forEach(node => collectIds(node)) + + return ids; + } + + updateNode(node: TreeNode, selected: boolean, updateChildren = true, updateParents = true, emit = true) { + if (node.selected === selected) { + // Nothing changed + return; + } + + // update the node an all children + node.selected = selected; + if (updateChildren) { + node.children.forEach(child => this.updateNode(child, selected, true, false, false)); + } + + // if we have a parent we might need to update + // the parent as well. + if (!!node.parent && updateParents) { + if (selected) { + // if we are now selected we might need to "select" the + // parent if all children are selected now. + const hasUnselected = node.parent.children.some(sibling => !sibling.selected); + if (!hasUnselected) { + // We need to update all parents but updating children + // is useless. + this.updateNode(node.parent, true, false, true, false); + } + } else if (node.parent.selected) { + // if we are unselected now we might need to "unselect" the parent + // but select siblings directly + const selectedSiblings = node.parent.children.filter(sibling => sibling.selected && sibling !== node); + this.updateNode(node.parent, false, false, true, false) + } + } + + if (emit) { + const ids = this.getIDs(); + this.selectedIDs = ids; + this._onChange(this.selectedIDs); + } + } + + /** @private TrackByFunction for tree nodes. */ + trackNode(_: number, node: TreeNode) { + return node.id; + } +} + diff --git a/desktop/angular/src/app/shared/config/filter-lists/index.ts b/desktop/angular/src/app/shared/config/filter-lists/index.ts new file mode 100644 index 00000000..07932b7e --- /dev/null +++ b/desktop/angular/src/app/shared/config/filter-lists/index.ts @@ -0,0 +1 @@ +export { FilterListComponent } from './filter-list'; diff --git a/desktop/angular/src/app/shared/config/generic-setting/generic-setting.html b/desktop/angular/src/app/shared/config/generic-setting/generic-setting.html new file mode 100644 index 00000000..7c2f9bd1 --- /dev/null +++ b/desktop/angular/src/app/shared/config/generic-setting/generic-setting.html @@ -0,0 +1,204 @@ +
+ + + +
+
+

+ + + + + + + + Saved {{ _setting?.RequiresRestart ? ' - Restart required' : (uiReloadRequired ? ' - Reload required' : '') }} + + + + + + + Invalid Value: {{ rejected }} + + + + + + + This feature requires a subscription. + +
+ + + + {{setting?.Key}} + + Beta + + Experimental + + Advanced + + Developer + + +
+ + + +
+ + + + + Quick Settings + + + + + {{quick.Name}} + + +
+ + + + + + + + +
+

This setting stacks on top of the following global setting:

+ + +
+
+ + + + + + + + + + + +
+

This setting stacks on top of the following global setting:

+ +
+
+ + + + + + + + +
+

This setting stacks on top of the following global setting:

+ + +
+
+ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + Inherited from Global Settings + + + App specific configuration + + +
+ +
+ + {{resetLabelText}} + +
+ + +
+ +

{{ _setting?.Name }}

+ + +
+
\ No newline at end of file diff --git a/desktop/angular/src/app/shared/config/generic-setting/generic-setting.scss b/desktop/angular/src/app/shared/config/generic-setting/generic-setting.scss new file mode 100644 index 00000000..14e2c9e5 --- /dev/null +++ b/desktop/angular/src/app/shared/config/generic-setting/generic-setting.scss @@ -0,0 +1,97 @@ +:host { + @apply block; + + &.ng-invalid { + @apply border border-red border-opacity-50; + } + + &.rejected { + .release-level.rejected { + opacity: 1; + } + } + + &.highlighted:not(.touched) { + .name { + animation: fade-color 5s ease-out; + } + } +} + +.stacked-values { + margin-top: 0.5rem; + opacity: 0.7; + @apply w-full; +} + +.unlock-button { + @apply flex w-6 h-6 rounded-full; + + justify-content: center; + align-items: center; + cursor: pointer; + + position: absolute; + right: calc(-1.5rem/2); + top: calc(50% - 1.5rem/2); + + &:hover { + @apply bg-blue; + } +} + +.description, +.help-text { + display: block; + @apply text-secondary; +} + +.help-text { + @apply mb-2; +} + +.notice { + display: block; + padding-left: 0.5rem; + padding-right: 0.5rem; + @apply mb-4; + @apply text-secondary; + + fa-icon { + @apply mr-2; + } +} + +.help-text { + @apply p-4; + @apply bg-cards-secondary; + @apply rounded; + + .toggle { + position: relative; + left: -0.25rem; + cursor: pointer; + + fa-icon { + @apply pr-1; + } + + &:hover { + @apply text-primary; + } + } +} + +@keyframes fade-color { + 0% { + @apply text-blue; + } + + 90% { + @apply text-blue; + } + + 100% { + @apply text-primary; + } +} diff --git a/desktop/angular/src/app/shared/config/generic-setting/generic-setting.ts b/desktop/angular/src/app/shared/config/generic-setting/generic-setting.ts new file mode 100644 index 00000000..4ff5a7f3 --- /dev/null +++ b/desktop/angular/src/app/shared/config/generic-setting/generic-setting.ts @@ -0,0 +1,715 @@ +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { TemplatePortal } from '@angular/cdk/portal'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, EventEmitter, HostBinding, Input, OnInit, Output, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { NgModel } from '@angular/forms'; +import { BaseSetting, ConfigService, ExpertiseLevel, ExpertiseLevelNumber, ExternalOptionHint, OptionType, PortapiService, QuickSetting, ReleaseLevel, SPNService, SettingValueType, UserProfile, WellKnown, applyQuickSetting } from '@safing/portmaster-api'; +import { SfngDialogRef, SfngDialogService } from '@safing/ui'; +import { Button } from 'js-yaml-loader!../../../i18n/helptexts.yaml'; +import { Subject } from 'rxjs'; +import { debounceTime, tap } from 'rxjs/operators'; +import { ActionIndicatorService } from '../../action-indicator'; +import { fadeInAnimation, fadeOutAnimation } from '../../animations'; +import { ExpertiseService } from '../../expertise/expertise.service'; +import { SPNAccountDetailsComponent } from '../../spn-account-details'; + +export interface SaveSettingEvent = any> { + key: string; + value: SettingValueType; + isDefault: boolean; + rejected?: (err: any) => void + accepted?: () => void +} + +@Component({ + selector: 'app-generic-setting', + templateUrl: './generic-setting.html', + exportAs: 'appGenericSetting', + styleUrls: ['./generic-setting.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + fadeInAnimation, + fadeOutAnimation + ] +}) +export class GenericSettingComponent> implements OnInit { + // + // Constants used in the template. + // + + readonly optionHint = ExternalOptionHint; + readonly expertiseNames = ExpertiseLevel + readonly expertise = ExpertiseLevelNumber; + readonly optionType = OptionType; + readonly releaseLevel = ReleaseLevel; + readonly wellKnown = WellKnown; + + @ViewChild('helpTemplate', { read: TemplateRef, static: true }) + helpTemplate: TemplateRef | null = null; + private helpDialogRef: SfngDialogRef | null = null; + + // Whether or not the user needs to upgrade his/her account before + // this setting is valid. + _upgradeRequired = false; + + /** + * Whether or not the component/setting is disabled and should + * be read-only. + */ + @Input() + @HostBinding('class.disabled') + set disabled(v: any) { + this._disabled = coerceBooleanProperty(v); + } + get disabled() { + return this._disabled || this._upgradeRequired; + } + private _disabled: boolean = false; + + /** Returns the symbolMap annoation for endpoint-lists */ + get symbolMap() { + return this.setting?.Annotations[WellKnown.EndpointListVerdictNames] || { + '+': 'Allow', + '-': 'Block' + }; + } + + /** Whether or not the setting should be in select mode */ + @Input() + set selectMode(v: any) { + this._selectMode = coerceBooleanProperty(v) + + if (!this.selectMode) { + this.selected = false; + this.selectedChange.next(false); + } + } + get selectMode() { return this._selectMode } + private _selectMode = false; + + /** Whether or not the setting has been selected */ + @Input() + set selected(v: any) { + this._selected = coerceBooleanProperty(v) + } + get selected() { return this._selected } + private _selected = false; + + /** Emits when the user (de-) selectes the setting. Can be used for two-way binding */ + @Output() + selectedChange = new EventEmitter(); + + /** Controls whether or not header with the setting name and success/failure markers is shown */ + @Input() + set showHeader(v: any) { + this._showHeader = coerceBooleanProperty(v); + } + get showHeader() { return this._showHeader } + private _showHeader = true; + + /** Controls whether or not the blue or red status borders are shown */ + @Input() + set enableActiveBorder(v: any) { + this._enableActiveBorder = coerceBooleanProperty(v); + } + get enableActiveBorder() { return this._enableActiveBorder } + private _enableActiveBorder = true; + + /** + * Whether or not the component should be displayed as "locked" + * when the default value is used (that is, no 'Value' property + * in the setting) + */ + @Input() + set lockDefaults(v: any) { + this._lockDefaults = coerceBooleanProperty(v); + } + get lockDefaults() { + return this._lockDefaults; + } + private _lockDefaults: boolean = false; + + /** The label to display in the reset-value button */ + @Input() + resetLabelText = 'Reset'; + + /** Emits an event whenever the setting should be saved. */ + @Output() + save = new EventEmitter>(); + + /** Wether or not stackable values should be displayed. */ + @Input() + set displayStackable(v: any) { + this._displayStackable = coerceBooleanProperty(v); + } + get displayStackable() { + return this._displayStackable; + } + private _displayStackable = false; + + /** + * Whether or not the help text is currently shown + */ + @Input() + set showHelp(v: any) { + this._showHelp = coerceBooleanProperty(v); + } + get showHelp() { + return this._showHelp; + } + private _showHelp = false; + + /** Used internally to publish save events. */ + private triggerSave = new Subject(); + + /** Whether or not the value was reset. */ + wasReset = false; + + /** Whether or not a save request was rejected */ + @HostBinding('class.rejected') + get rejected() { + return this._rejected; + } + private _rejected = null; + + @HostBinding('class.saved') + get changeAccepted() { + return this._changeAccepted; + } + private _changeAccepted = false; + + /** + * @private + * Returns the external option type hint from a setting. + * + * @param opt The setting for with to return the external option hint + */ + externalOptType(opt: S | null): ExternalOptionHint | null { + return opt?.Annotations?.[WellKnown.DisplayHint] || null; + } + + /** + * @private + * Returns whether or not a restart is pending for this setting + * to apply. + */ + get restartPending(): boolean { + return !!this._setting?.Annotations?.[WellKnown.RestartPending]; + } + + /** + * @private + * Returns whether or not a UI reload is required for this setting + * to apply + */ + get uiReloadRequired(): boolean { + return this._setting?.Annotations?.[WellKnown.RequiresUIReload] !== undefined; + } + + /** + * Returns true if the setting has been touched (modified) by the user + * since the component has been rendered. + */ + @HostBinding('class.touched') + get touched() { + return this._touched; + } + private _touched = false; + + /** + * Returns true if the settings is currently locked. + */ + @HostBinding('class.locked') + get isLocked() { + return (this.wasReset || !this.userConfigured) && this.lockDefaults; + } + + /** + * Returns true if the user has configured the setting on their + * own or if the default value is being used. + */ + @HostBinding('class.changed') + get userConfigured() { + return this.setting?.Value !== undefined; + } + + /** + * Returns true if the setting is dirty. That is, the user + * has changed the setting in the view but it has not yet + * been saved. + */ + @HostBinding('class.dirty') + get dirty() { + if (typeof this._currentValue !== 'object') { + return this._currentValue !== this._savedValue; + } + // JSON object (OptionType.StringArray) require will + // not be the same reference so we need to compare their + // string representations. That's a bit more costly but should + // still be fast enough. + // TODO(ppacher): calculate this only when required. + return JSON.stringify(this._currentValue) !== JSON.stringify(this._savedValue) + } + + /** + * Returns true if the setting is pristine. That is, the + * settings default value is used and the user has not yet + * changed the value inside the view. + */ + @HostBinding('class.pristine') + get pristine() { + return !this.dirty && !this.userConfigured + } + + /** A list of buttons for the tip-up */ + sfngTipUpButtons: Button[] = []; + + /** + * Unlock the setting if it is locked. Unlocking will + * emit the default value to be safed for the setting. + */ + unlock() { + if (!this.isLocked || !this.setting) { + return; + } + + this._touched = true; + this.wasReset = false; + let value = this.defaultValue; + + if (this.stackable) { + // TODO(ppacher): fix this one once string[] options can be + // stackable + value = [] as SettingValueType; + } + + this.updateValue(value, true); + // update the settings value now so the UI + // responds immediately. + this.setting!.Value = value; + } + + /** True if the current setting is stackable */ + get stackable() { + return !!this.setting?.Annotations[WellKnown.Stackable]; + } + + /** Wether or not stackable values should be shown right now */ + get showStackable() { + return this.stackable && this.displayStackable; + } + + /** + * @private + * Toggle Whether or not the help text is displayed + */ + toggleHelp() { + this.showHelp = !this.showHelp; + } + + /** + * @private + * Toggle Whether or not the setting is currently locked. + */ + toggleLock() { + if (this.isLocked) { + this.unlock(); + return; + } + + this.resetValue(); + } + + /** + * @private + * Closes the help dialog. + */ + closeHelpDialog() { + this.helpDialogRef?.close(); + } + + @ViewChild(NgModel, { static: false }) + model: NgModel | null = null; + + /** + * The actual setting that should be managed. + * The setter also updates the "currently" used + * value (which is either user configured or + * the default). See {@property userConfigured}. + */ + @Input() + set setting(s: S | null) { + this.sfngTipUpButtons = []; + + this._setting = s; + if (!s) { + this._currentValue = null; + return; + } + + if (this._setting?.Help) { + this.sfngTipUpButtons = [ + { + name: 'Show More', + action: { + ID: '', + Text: '', + Type: 'ui', + Run: async () => { + if (!this.helpTemplate) { + return; + } + + // close any existing help dialog for THIS setting. + if (!!this.helpDialogRef) { + this.helpDialogRef.close(); + } + + // Create a new dialog form the helpTemplate + const portal = new TemplatePortal(this.helpTemplate, this.viewRef); + const ref = this.dialog.create(portal, { + // we don't use a backdrop and make the dialog dragable so the user can + // move it somewhere else and keep it open while configuring the setting. + backdrop: false, + dragable: true, + }); + + // make sure we reset the helpDialogRef to null once it get's clsoed. + this.helpDialogRef = ref; + this.helpDialogRef.onClose.subscribe(() => { + // but only if helpDialogRef still points to the same + // dialog reference. Otherwise we got closed because the user + // opened a new one and helpDialogRef already points to the new + // dialog. + if (this.helpDialogRef === ref) { + this.helpDialogRef = null; + } + }); + }, + Payload: undefined, + }, + }, + ] + } + this.updateActualValue(); + } + get setting(): S | null { + return this._setting; + } + + /** + * The defaultValue input allows to overwrite the default + * value of the setting. + */ + @Input() + set defaultValue(val: SettingValueType) { + this._defaultValue = val; + this.updateActualValue(); + } + + get defaultValue() { + // Return cached value. + if (this._defaultValue !== null) { + return this._defaultValue; + } + + // Stackable options are displayed differently. + if (this.stackable) { + if (this.setting?.GlobalDefault === undefined && this.setting?.DefaultValue !== null) { + return this.setting?.DefaultValue; + } + return [] as SettingValueType; + } + + // Return global, then default value. + if (this.setting?.GlobalDefault !== undefined) { + return this.setting.GlobalDefault + } + return this.setting?.DefaultValue + } + + /* An optional default value overwrite */ + _defaultValue: SettingValueType | null = null; + + /* Whether or not the setting has been saved */ + saved = true; + + /* The settings value, updated by the setting() setter */ + _setting: S | null = null; + + /* The currently configured value. Updated by the setting() setter */ + _currentValue: SettingValueType | null = null; + + /* The currently saved value. Updated by the setting() setter */ + _savedValue: SettingValueType | null = null; + + /* Used to cache the value of a basic-setting because we only want to save that on blur */ + _basicSettingsValueCache: SettingValueType | null = null + + /** Whether or not the network rating system is enabled. */ + networkRatingEnabled$ = this.configService.networkRatingEnabled$; + + get expertiseLevel() { + return this.expertiseService.change; + } + + constructor( + private expertiseService: ExpertiseService, + private configService: ConfigService, + private portapi: PortapiService, + private dialog: SfngDialogService, + private changeDetectorRef: ChangeDetectorRef, + private actionIndicator: ActionIndicatorService, + private spn: SPNService, + private viewRef: ViewContainerRef, + private destryoRef: DestroyRef, + ) { } + + ngOnInit() { + this.triggerSave + .pipe( + debounceTime(500), + takeUntilDestroyed(this.destryoRef), + ) + .subscribe(() => this.emitSaveRequest()) + + // watch the SPN user profile so we know which feature_ids + // are available for the user. + this.spn.profile$ + .pipe(takeUntilDestroyed(this.destryoRef)) + .subscribe((profile: UserProfile | null) => { + let value = this.setting?.Annotations[WellKnown.RequiresFeatureID] + if (value === undefined) { + this._upgradeRequired = false; + } else { + if (!Array.isArray(value)) { + value = [value]; + } + + this._upgradeRequired = value.some(val => !(profile?.current_plan?.feature_ids || []).includes(val)) + } + + this.changeDetectorRef.markForCheck(); + }) + } + + /** + * @private + * Resets the value of setting by discarding any user + * configured values and reverting back to the default + * value. + */ + resetValue() { + if (!this._setting) { + return; + } + this._touched = true; + + this._currentValue = this.defaultValue; + this.wasReset = true; + + this.triggerSave.next(); + } + + /** + * @private + * Aborts/reverts the current change to the value that's + * already saved. + */ + abortChange() { + this._currentValue = this._savedValue; + this._touched = true; + this._rejected = null; + } + + /** + * @private + * Update the current value by applying a quick-setting. + * + * @param qs The quick-settting to apply + */ + applyQuickSetting(qs: QuickSetting>) { + if (this.disabled) { + return; + } + + const value = applyQuickSetting(this._currentValue, qs); + if (value === null) { + return; + } + + this.updateValue(value, true); + } + + openAccountDetails() { + this.dialog.create(SPNAccountDetailsComponent, { + autoclose: true, + backdrop: 'light' + }) + } + + restartNow() { + if (this._setting?.RequiresRestart) { + this.dialog.confirm({ + header: 'Restart Portmaster', + message: 'Do you want to restart the Portmaster now?', + buttons: [ + { + id: 'no', + text: 'Maybe Later', + class: 'outline', + }, + { + id: 'restart', + text: 'Restart', + class: 'danger' + } + ] + }) + .onAction('restart', () => + this.portapi.restartPortmaster() + .subscribe(this.actionIndicator.httpObserver( + 'Restarting ...', + 'Failed to Restart', + )) + ) + .onAction('no', () => { + this._changeAccepted = false; + this.changeDetectorRef.markForCheck(); + }); + + return; + } + + if (this.uiReloadRequired) { + this.portapi.reloadUI() + .pipe( + tap(() => { + setTimeout(() => window.location.reload(), 1000) + }) + ) + .subscribe(this.actionIndicator.httpObserver( + 'Reloading UI ...', + 'Failed to Reload UI', + )) + } + } + + /** + * Emits a save request to the parent component. + */ + private _saveInterval: any; + private emitSaveRequest() { + const isDefault = this.wasReset; + let value = this._setting!['Value']; + + if (isDefault) { + delete (this._setting!['Value']); + } else { + this._setting!.Value = this._currentValue; + } + + + let wasReset = this.wasReset; + this.wasReset = false; + this._rejected = null; + this._changeAccepted = false; + if (!!this._saveInterval) { + clearTimeout(this._saveInterval); + } + + this.save.next({ + key: this.setting!.Key, + isDefault: isDefault, + value: this._setting!.Value, + rejected: (err: any) => { + this._setting!['Value'] = value; + this._rejected = err; + this.changeDetectorRef.markForCheck(); + }, + accepted: () => { + if (!wasReset) { + this._changeAccepted = true; + // if no restart is required fade the "✔️ Saved" out after + // a few seconds. + if (!this._setting?.RequiresRestart) { + this._saveInterval = setTimeout(() => { + this._changeAccepted = false; + this._saveInterval = null; + this.changeDetectorRef.markForCheck(); + }, 4000); + } + } + + this.changeDetectorRef.markForCheck(); + + } + }) + } + + /** + * @private + * Used in our view as a ngModelChange callback to + * update the value. + * + * @param value The new value as emitted by the view + */ + updateValue(value: SettingValueType, save = false) { + this._touched = true; + + this._changeAccepted = false; + this._rejected = null; + if (!!this._saveInterval) { + clearTimeout(this._saveInterval); + } + + if (save) { + + this._currentValue = value; + this.triggerSave.next(); + } else { + this._basicSettingsValueCache = value; + } + } + + /** + * @private + * A list of quick-settings available for the setting. + * The getter makes sure to always return an array. + */ + get quickSettings(): QuickSetting>[] { + if (!this.setting || !this.setting.Annotations[WellKnown.QuickSetting]) { + return []; + } + + const quickSettings = this.setting.Annotations[WellKnown.QuickSetting]!; + + return Array.isArray(quickSettings) + ? quickSettings + : [quickSettings]; + } + + /** + * Determine the current, actual value of the setting + * by taking the settings Value, default Value or global + * default into account. + */ + private updateActualValue() { + if (!this.setting) { + return + } + + this.wasReset = false; + + const s = this.setting; + + const value = s.Value === undefined + ? this.defaultValue + : s.Value; + + + this._currentValue = value; + this._savedValue = value; + this._basicSettingsValueCache = value; + } +} diff --git a/desktop/angular/src/app/shared/config/generic-setting/index.ts b/desktop/angular/src/app/shared/config/generic-setting/index.ts new file mode 100644 index 00000000..0fbe8492 --- /dev/null +++ b/desktop/angular/src/app/shared/config/generic-setting/index.ts @@ -0,0 +1 @@ +export * from './generic-setting'; diff --git a/desktop/angular/src/app/shared/config/import-dialog/cursor.ts b/desktop/angular/src/app/shared/config/import-dialog/cursor.ts new file mode 100644 index 00000000..1ab638ee --- /dev/null +++ b/desktop/angular/src/app/shared/config/import-dialog/cursor.ts @@ -0,0 +1,90 @@ +// Credit to Liam (Stack Overflow) +// https://stackoverflow.com/a/41034697/3480193 +export class Cursor { + static getCurrentCursorPosition(parentElement: Node) { + var selection = window.getSelection(), + charCount = -1, + node; + + if (selection?.focusNode) { + if (Cursor._isChildOf(selection.focusNode, parentElement)) { + node = selection.focusNode; + charCount = selection.focusOffset; + + while (node) { + if (node === parentElement) { + break; + } + + if (node.previousSibling) { + node = node.previousSibling; + charCount += node.textContent?.length || 0 + } else { + node = node.parentNode; + if (node === null) { + break; + } + } + } + } + } + + return charCount; + } + + static setCurrentCursorPosition(chars: number, element: Node) { + if (chars >= 0) { + var selection = window.getSelection(); + + let range = Cursor._createRange(element, { count: chars }); + + if (range) { + range.collapse(false); + selection?.removeAllRanges(); + selection?.addRange(range); + } + } + } + + static _createRange(node: Node, chars: { count: number }, range?: Range): Range { + if (!range) { + range = document.createRange() + range.selectNode(node); + range.setStart(node, 0); + } + + if (chars.count === 0) { + range.setEnd(node, chars.count); + } else if (node && chars.count > 0) { + if (node.nodeType === Node.TEXT_NODE) { + if (node.textContent!.length < chars.count) { + chars.count -= node.textContent!.length; + } else { + range.setEnd(node, chars.count); + chars.count = 0; + } + } else { + for (var lp = 0; lp < node.childNodes.length; lp++) { + range = Cursor._createRange(node.childNodes[lp], chars, range); + + if (chars.count === 0) { + break; + } + } + } + } + + return range; + } + + static _isChildOf(node: Node, parentElement: Node) { + while (node !== null) { + if (node === parentElement) { + return true; + } + node = node.parentNode!; + } + + return false; + } +} diff --git a/desktop/angular/src/app/shared/config/import-dialog/import-dialog.component.html b/desktop/angular/src/app/shared/config/import-dialog/import-dialog.component.html new file mode 100644 index 00000000..c55709d4 --- /dev/null +++ b/desktop/angular/src/app/shared/config/import-dialog/import-dialog.component.html @@ -0,0 +1,99 @@ +
+

+ Import {{ dialogRef.data.type === "setting" ? "Settings" : "Profile" }} +

+ + +
+ +Please paste the "Export Content" or use "Choose File" to select one from + your hard disk. + +

+
+
+ Configuration + +
+
+ + +
+ +
+ + +
+ + +
+ + +
+ +
+ + +
+
+
+ +
+ Warning + +
+ + + + {{ errorMessage }} +
+ +
    +
  • + This export contains unknown settings. To import it, you must enable + "Allow unknown settings". +
  • + +
  • + {{ + dialogRef.data.type === "setting" + ? "This export will overwrite settings that have been changed by you." + : "This export will overwrite an existing profile." + }} + + + And deletes {{ count }} previously merged profile{{ count > 1 ? 's' : '' }} + +
  • + +
  • + This export will require a restart of the Portmaster to take effect. +
  • +
+
+ +
+ + + +
+ + \ No newline at end of file diff --git a/desktop/angular/src/app/shared/config/import-dialog/import-dialog.component.ts b/desktop/angular/src/app/shared/config/import-dialog/import-dialog.component.ts new file mode 100644 index 00000000..57a5713c --- /dev/null +++ b/desktop/angular/src/app/shared/config/import-dialog/import-dialog.component.ts @@ -0,0 +1,201 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + ViewChild, + inject, +} from '@angular/core'; +import { ImportResult, PortapiService, ProfileImportResult } from '@safing/portmaster-api'; +import { SFNG_DIALOG_REF, SfngDialogRef } from '@safing/ui'; +import { ActionIndicatorService } from '../../action-indicator'; +import { getSelectionOffset, setSelectionOffset } from './selection'; +import { Observable } from 'rxjs'; + +export interface ImportConfig { + key: string; + type: 'setting' | 'profile'; +} + +@Component({ + templateUrl: './import-dialog.component.html', + styles: [ + ` + :host { + @apply flex flex-col gap-2 overflow-hidden; + min-height: 24rem; + min-width: 24rem; + max-height: 40rem; + max-width: 40rem; + } + `, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ImportDialogComponent { + readonly dialogRef: SfngDialogRef< + ImportDialogComponent, + unknown, + ImportConfig + > = inject(SFNG_DIALOG_REF); + + private readonly portapi = inject(PortapiService); + private readonly uai = inject(ActionIndicatorService); + private readonly cdr = inject(ChangeDetectorRef); + + @ViewChild('codeBlock', { static: true, read: ElementRef }) + codeBlockElement!: ElementRef; + + result: ImportResult | ProfileImportResult | null = null; + reset = false; + allowUnknown = false; + triggerRestart = false; + allowReplace = false; + + get replacedProfiles() { + if (this.result === null) { + return [] + } + + if ('replacesProfiles' in this.result) { + return this.result.replacesProfiles || []; + } + + return []; + } + + errorMessage: string = ''; + + get scope() { + return this.dialogRef.data; + } + + onBlur() { + const text = this.codeBlockElement.nativeElement.innerText; + this.updateAndValidate(text); + } + + onPaste(event: ClipboardEvent) { + event.stopPropagation(); + event.preventDefault(); + + // Get pasted data via clipboard API + const clipboardData = event.clipboardData || (window as any).clipboardData; + const text = clipboardData.getData('Text'); + + this.updateAndValidate(text); + } + + import() { + const text = this.codeBlockElement.nativeElement.innerText; + + let saveFunc: Observable; + + if (this.dialogRef.data.type === 'setting') { + saveFunc = this.portapi.importSettings( + text, + this.dialogRef.data.key, + 'text/yaml', + this.reset, + this.allowUnknown + ); + } else { + saveFunc = this.portapi.importProfile( + text, + 'text/yaml', + this.reset, + this.allowUnknown, + this.allowReplace + ); + } + + saveFunc.subscribe({ + next: (result) => { + let msg = ''; + if (result.restartRequired) { + if (this.triggerRestart) { + this.portapi.restartPortmaster().subscribe(); + msg = 'Portmaster will be restarted now.'; + } else { + msg = 'Please restart Portmaster to apply the new settings.'; + } + } + + this.uai.success('Settings Imported Successfully', msg); + this.dialogRef.close(); + }, + error: (err) => { + this.uai.error( + 'Failed To Import Settings', + this.uai.getErrorMessgae(err) + ); + }, + }); + } + + updateAndValidate(content: string) { + const [start, end] = getSelectionOffset( + this.codeBlockElement.nativeElement + ); + + const p = (window as any).Prism; + const blob = p.highlight(content, p.languages.yaml, 'yaml'); + this.codeBlockElement.nativeElement.innerHTML = blob; + + setSelectionOffset(this.codeBlockElement.nativeElement, start, end); + + if (content === '') { + return; + } + + window.getSelection()?.removeAllRanges(); + + let validateFunc: Observable; + + if (this.dialogRef.data.type === 'setting') { + validateFunc = this.portapi.validateSettingsImport( + content, + this.dialogRef.data.key, + 'text/yaml' + ); + } else { + validateFunc = this.portapi.validateProfileImport(content, 'text/yaml'); + } + + validateFunc.subscribe({ + next: (result) => { + this.result = result; + this.errorMessage = ''; + + this.cdr.markForCheck(); + }, + error: (err) => { + const msg = this.uai.getErrorMessgae(err); + this.errorMessage = msg; + this.result = null; + + this.cdr.markForCheck(); + }, + }); + } + + loadFile(event: Event) { + const file: File = (event.target as any).files[0]; + if (!file) { + this.updateAndValidate(''); + + return; + } + + const reader = new FileReader(); + + reader.onload = (data) => { + (event.target as any).value = ''; + + let content = (data.target as any).result; + this.updateAndValidate(content); + }; + + reader.readAsText(file); + } +} diff --git a/desktop/angular/src/app/shared/config/import-dialog/selection.ts b/desktop/angular/src/app/shared/config/import-dialog/selection.ts new file mode 100644 index 00000000..e7018115 --- /dev/null +++ b/desktop/angular/src/app/shared/config/import-dialog/selection.ts @@ -0,0 +1,185 @@ +/** return true if node found */ +function searchNode( + container: Node, + startNode: Node, + predicate: (node: Node) => boolean, + excludeSibling?: boolean, +): boolean { + if (predicate(startNode as Text)) { + return true + } + + for (let i = 0, len = startNode.childNodes.length; i < len; i++) { + if (searchNode(startNode, startNode.childNodes[i], predicate, true)) { + return true + } + } + + if (!excludeSibling) { + let parentNode = startNode + while (parentNode && parentNode !== container) { + let nextSibling = parentNode.nextSibling + while (nextSibling) { + if (searchNode(container, nextSibling, predicate, true)) { + return true + } + nextSibling = nextSibling.nextSibling + } + parentNode = parentNode.parentNode! + } + } + + return false +} + +function createRange(container: Node, start: number, end: number): Range { + let startNode: any; + + searchNode(container, container, node => { + if (node.nodeType === Node.TEXT_NODE) { + const dataLength = (node as Text).data.length + if (start <= dataLength) { + startNode = node + return true + } + start -= dataLength + end -= dataLength + } + + return false + }) + + let endNode: any; + + if (startNode) { + searchNode(container, startNode, node => { + if (node.nodeType === Node.TEXT_NODE) { + const dataLength = (node as Text).data.length + if (end <= dataLength) { + endNode = node + return true + } + end -= dataLength + } + + return false + }) + } + + const range = document.createRange() + if (startNode) { + if (start < startNode.data.length) { + range.setStart(startNode, start) + } else { + range.setStartAfter(startNode) + } + } else { + if (start === 0) { + range.setStart(container, 0) + } else { + range.setStartAfter(container) + } + } + + if (endNode) { + if (end < endNode.data.length) { + range.setEnd(endNode, end) + } else { + range.setEndAfter(endNode) + } + } else { + if (end === 0) { + range.setEnd(container, 0) + } else { + range.setEndAfter(container) + } + } + + return range +} + +export function setSelectionOffset(node: Node, start: number, end: number) { + const range = createRange(node, start, end) + const selection = window.getSelection()! + selection.removeAllRanges() + selection.addRange(range) +} + + +function getAbsoluteOffset(container: Node, offset: number) { + if (container.nodeType === Node.TEXT_NODE) { + return offset + } + + let absoluteOffset = 0 + for (let i = 0, len = Math.min(container.childNodes.length, offset); i < len; i++) { + const childNode = container.childNodes[i] + searchNode(childNode, childNode, node => { + if (node.nodeType === Node.TEXT_NODE) { + absoluteOffset += (node as Text).data.length + } + return false + }) + } + + return absoluteOffset +} + +export function getSelectionOffset(container: Node): [number, number] { + let start = 0 + let end = 0 + + const selection = window.getSelection()! + for (let i = 0, len = selection.rangeCount; i < len; i++) { + const range = selection.getRangeAt(i) + if (range.intersectsNode(container)) { + const startNode = range.startContainer + searchNode(container, container, node => { + if (startNode === node) { + start += getAbsoluteOffset(node, range.startOffset) + return true + } + + const dataLength = node.nodeType === Node.TEXT_NODE + ? (node as Text).data.length + : 0 + + start += dataLength + end += dataLength + + return false + }) + + const endNode = range.endContainer + searchNode(container, startNode, node => { + if (endNode === node) { + end += getAbsoluteOffset(node, range.endOffset) + return true + } + + const dataLength = node.nodeType === Node.TEXT_NODE + ? (node as Text).data.length + : 0 + + end += dataLength + + return false + }) + + break + } + } + + return [start, end] +} + +export function getInnerText(container: Node): string { + const buffer: any = [] + searchNode(container, container, node => { + if (node.nodeType === Node.TEXT_NODE) { + buffer.push((node as Text).data) + } + return false + }) + return buffer.join('') +} diff --git a/desktop/angular/src/app/shared/config/index.ts b/desktop/angular/src/app/shared/config/index.ts new file mode 100644 index 00000000..d71f0297 --- /dev/null +++ b/desktop/angular/src/app/shared/config/index.ts @@ -0,0 +1,8 @@ +export * from './basic-setting'; +export * from './config-settings'; +export * from './config.module'; +export * from './filter-lists'; +export * from './generic-setting'; +export * from './ordererd-list'; +export * from './rule-list'; +export * from './safe.pipe'; diff --git a/desktop/angular/src/app/shared/config/ordererd-list/index.ts b/desktop/angular/src/app/shared/config/ordererd-list/index.ts new file mode 100644 index 00000000..e8849b33 --- /dev/null +++ b/desktop/angular/src/app/shared/config/ordererd-list/index.ts @@ -0,0 +1,2 @@ +export { OrderedListComponent } from './ordered-list'; +export { OrderedListItemComponent } from './item'; \ No newline at end of file diff --git a/desktop/angular/src/app/shared/config/ordererd-list/item.html b/desktop/angular/src/app/shared/config/ordererd-list/item.html new file mode 100644 index 00000000..8550145b --- /dev/null +++ b/desktop/angular/src/app/shared/config/ordererd-list/item.html @@ -0,0 +1,14 @@ +
+ + {{value}} + + + + + + +
+ + +
+
diff --git a/desktop/angular/src/app/shared/config/ordererd-list/item.scss b/desktop/angular/src/app/shared/config/ordererd-list/item.scss new file mode 100644 index 00000000..169a61c5 --- /dev/null +++ b/desktop/angular/src/app/shared/config/ordererd-list/item.scss @@ -0,0 +1,56 @@ +:host { + @apply flex outline-none; + @apply space-x-2; + + &>* { + @apply rounded; + @apply bg-gray-300; + } +} + +div.value { + @apply border-gray-500 border; + @apply p-1; + @apply px-2; + + &.edit { + @apply p-0; + @apply bg-gray-400; + + input { + margin: 0; + width: auto; + flex-grow: 1; + border: none; + @apply shadow-none; + } + + input:focus+.buttons { + @apply bg-gray-500 border-gray-600 bg-opacity-75 border-opacity-75; + } + } + + flex-grow : 1; + display : flex; + justify-content: space-between; + align-items : center; + + .buttons { + flex-shrink: 0; + height: 100%; + width: 4rem; + @apply flex items-center justify-evenly; + + fa-icon { + cursor: pointer; + @apply text-primary; + @apply p-1; + opacity: 0.7; + font-size: 0.6rem; + + &:hover { + opacity: 1; + } + } + } +} diff --git a/desktop/angular/src/app/shared/config/ordererd-list/item.ts b/desktop/angular/src/app/shared/config/ordererd-list/item.ts new file mode 100644 index 00000000..eefb4e3b --- /dev/null +++ b/desktop/angular/src/app/shared/config/ordererd-list/item.ts @@ -0,0 +1,87 @@ +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; + +@Component({ + selector: 'app-ordered-list-item', + templateUrl: './item.html', + styleUrls: ['./item.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OrderedListItemComponent implements OnInit { + @Input() + set readonly(v: any) { + this._readonly = coerceBooleanProperty(v); + } + get readonly() { + return this._readonly; + } + private _readonly = false; + + @Input() + set value(v: string) { + this._value = v; + this._savedValue = v; + } + get value() { + return this._value; + } + _value = ''; + + private _savedValue = ''; + + @Output() + readonly valueChange = new EventEmitter(); + + @Output() + readonly delete = new EventEmitter(); + + @Input() + set edit(v: any) { + this._edit = coerceBooleanProperty(v); + } + get edit() { + return this._edit; + } + _edit = false; + + @Output() + readonly editChange = new EventEmitter(); + + ngOnInit() { + if (this._value === '' && this._savedValue === '') { + this.edit = true; + } + } + + toggleEdit() { + const wasEdit = this._edit; + this._edit = !wasEdit; + this.editChange.next(this._edit); + + if (!wasEdit) { + return; + } + + if (this._value !== this._savedValue) { + this._value = this._value.trim() + + this.valueChange.next(this.value); + this._savedValue = this._value; + } + this.changeDetectorRef.markForCheck(); + } + + reset() { + if (this._edit) { + if (this._value !== '' || this._savedValue !== '') { + this._value = this._savedValue; + this.changeDetectorRef.markForCheck(); + return; + } + } + + this.delete.next(); + } + + constructor(private changeDetectorRef: ChangeDetectorRef) { } +} diff --git a/desktop/angular/src/app/shared/config/ordererd-list/ordered-list.html b/desktop/angular/src/app/shared/config/ordererd-list/ordered-list.html new file mode 100644 index 00000000..fa043cb5 --- /dev/null +++ b/desktop/angular/src/app/shared/config/ordererd-list/ordered-list.html @@ -0,0 +1,23 @@ +
+
+ + + + + +
+
+ +
+ +
diff --git a/desktop/angular/src/app/shared/config/ordererd-list/ordered-list.scss b/desktop/angular/src/app/shared/config/ordererd-list/ordered-list.scss new file mode 100644 index 00000000..d4c1c086 --- /dev/null +++ b/desktop/angular/src/app/shared/config/ordererd-list/ordered-list.scss @@ -0,0 +1,77 @@ +:host { + outline: none; +} + +.item, +.cdk-drag-preview { + display: flex; + align-items: center; + padding: 3px; + + fa-icon { + cursor: pointer; + @apply text-tertiary; + @apply text-lg; + @apply mr-2; + } + + app-ordered-list-item { + flex-grow: 1; + } +} + +.cdk-drag-placeholder { + left: -4px; + padding: 1px; + padding-left: 4px; +} + +// TODO(ppacher9): move this transition to a mixin +.list-items.cdk-drop-list-dragging .list:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.cdk-drag-preview { + left: -4px; + padding: 1px; + padding-left: 4px; +} + +.button-list { + @apply mt-2; + @apply ml-8; +} + +.new-entry { + position: relative; + cursor: pointer; + @apply w-full; + @apply rounded; + @apply p-1; + @apply border-2; + @apply border-dashed; + @apply border-buttons-light; + @apply bg-background; + @apply text-secondary; + + span { + @apply font-medium; + } + + fa-icon { + font-size: 1rem; + } + + &:hover { + @apply text-primary; + @apply bg-cards-secondary; + + span { + @apply text-primary; + } + } + + display : flex; + align-items : center; + justify-content: center; +} diff --git a/desktop/angular/src/app/shared/config/ordererd-list/ordered-list.ts b/desktop/angular/src/app/shared/config/ordererd-list/ordered-list.ts new file mode 100644 index 00000000..0655ccb5 --- /dev/null +++ b/desktop/angular/src/app/shared/config/ordererd-list/ordered-list.ts @@ -0,0 +1,111 @@ +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, HostBinding, HostListener, Input } from "@angular/core"; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + + +@Component({ + selector: 'app-ordered-list', + templateUrl: './ordered-list.html', + styleUrls: ['./ordered-list.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => OrderedListComponent), + multi: true, + } + ] +}) +export class OrderedListComponent implements ControlValueAccessor { + @HostBinding('tabindex') + readonly tabindex = 0; + + @HostListener('blur') + onBlur() { + this.onTouch(); + } + + @Input() + set readonly(v: any) { + this._readonly = coerceBooleanProperty(v); + } + get readonly() { + return this._readonly; + } + _readonly = false; + + @Input() + set fixedOrder(v: any) { + this._fixedOrder = coerceBooleanProperty(v); + } + get fixedOrder() { + return this._fixedOrder; + } + private _fixedOrder = false; + + entries: string[] = []; + + constructor(private changeDetector: ChangeDetectorRef) { } + + updateValue(index: number, newValue: string) { + // we need to make a new object copy here. + this.entries = [ + ...this.entries, + ]; + + this.entries[index] = newValue; + this.onChange(this.entries); + } + + deleteEntry(index: number) { + this.entries = [...this.entries]; + this.entries.splice(index, 1); + this.onChange(this.entries); + } + + addEntry() { + // if there's already one empty entry abort + if (this.entries.some(e => e.trim() === '')) { + return; + } + + this.entries = [...this.entries]; + this.entries.push(''); + //this.onChange(this.entries); + } + + writeValue(value: string[]) { + this.entries = value; + + this.changeDetector.markForCheck(); + } + + onChange = (_: string[]): void => { }; + registerOnChange(fn: (value: string[]) => void) { + this.onChange = fn; + } + + onTouch = (): void => { }; + registerOnTouched(fn: () => void) { + this.onTouch = fn; + } + + drop(event: CdkDragDrop) { + if (this._readonly) { + return; + } + + // create a copy of the array + this.entries = [...this.entries]; + moveItemInArray(this.entries, event.previousIndex, event.currentIndex); + + this.changeDetector.markForCheck(); + this.onChange(this.entries); + } + + trackBy(idx: number, value: string) { + return `${value}`; + } +} + diff --git a/desktop/angular/src/app/shared/config/rule-list/index.ts b/desktop/angular/src/app/shared/config/rule-list/index.ts new file mode 100644 index 00000000..a2d41fde --- /dev/null +++ b/desktop/angular/src/app/shared/config/rule-list/index.ts @@ -0,0 +1,2 @@ +export * from './list-item'; +export * from './rule-list'; diff --git a/desktop/angular/src/app/shared/config/rule-list/list-item.html b/desktop/angular/src/app/shared/config/rule-list/list-item.html new file mode 100644 index 00000000..34eeae30 --- /dev/null +++ b/desktop/angular/src/app/shared/config/rule-list/list-item.html @@ -0,0 +1,29 @@ +
+ + {{ symbolMap["+"] }} + {{ symbolMap["-"] }} + + + + + {{ symbolMap["+"] }} + {{ symbolMap["-"] }} + + +
+
+ + {{ display }} + + + + + + +
+ + + +
+ +
diff --git a/desktop/angular/src/app/shared/config/rule-list/list-item.scss b/desktop/angular/src/app/shared/config/rule-list/list-item.scss new file mode 100644 index 00000000..814d311b --- /dev/null +++ b/desktop/angular/src/app/shared/config/rule-list/list-item.scss @@ -0,0 +1,65 @@ +:host { + display: flex; + outline: none; + @apply space-x-2; + + &>* { + @apply rounded; + @apply bg-gray-300; + } +} + +div.action { + @apply border-gray-500 border; + flex-shrink: 0; + min-width: 6rem; + text-align: center; +} + +div.value { + @apply border-gray-500 border; + @apply p-1.5; + @apply px-2; + + &.edit { + @apply p-0; + @apply bg-gray-400; + + input { + margin: 0; + width: auto; + height: 100%; + flex-grow: 1; + border: none; + @apply shadow-none; + } + + input:focus+.buttons { + @apply bg-gray-500 border-gray-600 bg-opacity-75 border-opacity-75; + } + } + + flex-grow : 1; + display : flex; + justify-content: space-between; + align-items : center; + + .buttons { + flex-shrink: 0; + height: 100%; + width: 4rem; + @apply flex items-center justify-evenly; + + fa-icon { + cursor: pointer; + @apply text-primary; + @apply p-1; + opacity: 0.7; + font-size: 0.6rem; + + &:hover { + opacity: 1; + } + } + } +} diff --git a/desktop/angular/src/app/shared/config/rule-list/list-item.ts b/desktop/angular/src/app/shared/config/rule-list/list-item.ts new file mode 100644 index 00000000..09a6beaf --- /dev/null +++ b/desktop/angular/src/app/shared/config/rule-list/list-item.ts @@ -0,0 +1,221 @@ +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostBinding, Input, OnInit, Output } from '@angular/core'; +import { fadeInAnimation, fadeOutAnimation } from '../../animations'; + +@Component({ + selector: 'app-rule-list-item', + templateUrl: 'list-item.html', + styleUrls: ['list-item.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + fadeInAnimation, + fadeOutAnimation + ] +}) +export class RuleListItemComponent implements OnInit { + /** The host element is going to fade in/out */ + @HostBinding('@fadeIn') + @HostBinding('@fadeOut') + readonly animation = true; + + @Input() + symbolMap: { [key: string]: string } = {} + + /** + * The current value (rule) displayed by this component. + * Supports two-way bindings. + */ + @Input() + set value(v: string) { + this.updateValue(v); + this._savedValue = this._value; + } + private _value = ''; + + /** The last actually saved value of this rule. Required for resets */ + private _savedValue = ''; + + /** + * Emits whenever the rule value changes. + * Supports two-way-bindings on ([value]) + */ + @Output() + valueChange = new EventEmitter(); + + /** Whether or not the rule list item is selected */ + @Input() + set selected(v: any) { + this._selected = coerceBooleanProperty(v) + } + get selected() { + return this._selected; + } + private _selected = false; + + @Output() + selectedChange = new EventEmitter(); + + /** + * Whether or not the component is in edit mode. + * Supports two-way-bindings on ([edit]) + */ + @Input() + set edit(v: any) { + this._edit = coerceBooleanProperty(v); + } + get edit() { + return this._edit; + } + private _edit: boolean = false; + + /** + * Emits whenever the component switch to or away from edit + * mode. + * Supports two-way-bindings on ([edit]) + */ + @Output() + editChange = new EventEmitter(); + + /** + * Whether or not the component should be in read-only mode. + */ + @Input() + set readonly(v: any) { + this._readonly = coerceBooleanProperty(v); + } + get readonly() { + return this._readonly; + } + private _readonly: boolean = false; + + /** + * Emits when the user presses the delete button of + * this rule component. + */ + @Output() + delete = new EventEmitter(); + + /** @private Whether or not this rule is a "Allow" rule - we default to allow since this is what most rules are used for */ + isAllow = true; + + /** @private Whether or not this rule is a "Deny" rule */ + isBlock = false; + + /** @private the actually displayed rule value (without the verdict) */ + display = ''; + + /** @private the character representation of the current verdict */ + get currentAction() { + if (this.isBlock) { + return '-'; + } + if (this.isAllow) { + return '+'; + } + return ''; + } + + constructor(private cdr: ChangeDetectorRef) { } + + ngOnInit() { + // new entries always start in edit mode + if (!this.isAllow && !this.isBlock) { + this._edit = true; + } + } + + /** + * @private + * Toggle between edit and view mode. When switching from + * edit to view mode, the current value is emitted to the + * parent element in case it has been changed. + */ + toggleEdit() { + if (this._edit) { + // do nothing if the rule is obviously invalid (no verdict or value). + if (this.display === '' || !(this.isAllow || this.isBlock)) { + return; + } + + if (this._value !== this._savedValue) { + this.valueChange.next(this._value); + } + } + + this._edit = !this._edit; + this.editChange.next(this._edit); + } + + toggleSelection() { + this.selected = !this.selected; + this.selectedChange.next(this.selected); + + this.cdr.markForCheck(); + } + + /** + * @private + * Sets the new rule action. Used as a callback in the drop-down. + * + * @param action The new action + */ + setAction(action: '+' | '-') { + this.updateValue(`${action} ${this.display}`); + } + + /** + * @private + * Update the actual value of the rule. + * + * @param entity The new rule value + */ + setEntity(entity: string) { + const action = this.isAllow ? '+' : '-'; + this.updateValue(`${action} ${entity}`); + } + + /** + * @private + * + * Reset the value to it's previously saved value if it was changed. + * If the value is unchanged a reset counts as a delete and triggers + * on our delete output. + */ + reset() { + if (this._edit) { + // if the user did not change anything we can immediately + // delete it. + if (this._savedValue !== '') { + this.value = this._savedValue; + this._edit = false; + return; + } + } + + this.delete.next(); + } + + /** + * Updates our internal states to correctly display the rule. + * + * @param v The actual rule value + */ + private updateValue(v: string) { + this._value = v.trim(); + switch (this._value[0]) { + case '+': + this.isAllow = true; + this.isBlock = false; + break; + case '-': + this.isAllow = false; + this.isBlock = true; + break; + default: + // not yet set + this.isBlock = this.isAllow = false; + } + + this.display = this._value.slice(1).trim(); + } +} diff --git a/desktop/angular/src/app/shared/config/rule-list/rule-list.html b/desktop/angular/src/app/shared/config/rule-list/rule-list.html new file mode 100644 index 00000000..3f7115c3 --- /dev/null +++ b/desktop/angular/src/app/shared/config/rule-list/rule-list.html @@ -0,0 +1,46 @@ +
+
+ + + + + +
+
+ +
+
+ No entries available +
+ + +
+ +
+ + + {{ selectedItems.length }} Rule{{ selectedItems.length > 1 ? 's' : ''}} selected + + + + + + + + Remove Rules + Cancel + +
diff --git a/desktop/angular/src/app/shared/config/rule-list/rule-list.scss b/desktop/angular/src/app/shared/config/rule-list/rule-list.scss new file mode 100644 index 00000000..23bf7034 --- /dev/null +++ b/desktop/angular/src/app/shared/config/rule-list/rule-list.scss @@ -0,0 +1,75 @@ +:host { + outline: none; +} + +.item, +.cdk-drag-preview { + display: flex; + align-items: center; + padding: 3px; + + fa-icon { + cursor: pointer; + @apply text-tertiary; + @apply text-lg; + @apply mr-2; + } + + app-rule-list-item { + flex-grow: 1; + } +} + +.cdk-drag-placeholder { + left: -4px; + padding: 1px; + padding-left: 4px; +} + +// TODO(ppacher9): move this transition to a mixin +.list-items.cdk-drop-list-dragging .list:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.cdk-drag-preview { + left: -4px; + padding: 1px; + padding-left: 4px; +} + +.button-list { + @apply mt-2; + @apply ml-8; +} + +.dotted { + @apply w-full; + @apply rounded; + @apply p-1; + @apply border-2; + @apply border-dashed; + @apply border-buttons-light; + @apply bg-background; + @apply text-secondary; + + display: flex; + align-items: center; + justify-content: center; + + span { + @apply font-medium; + } +} + +.new-entry { + cursor: pointer; + + &:hover { + @apply text-primary; + @apply bg-gray-300; + + span { + @apply text-primary; + } + } +} diff --git a/desktop/angular/src/app/shared/config/rule-list/rule-list.ts b/desktop/angular/src/app/shared/config/rule-list/rule-list.ts new file mode 100644 index 00000000..f5ac6c86 --- /dev/null +++ b/desktop/angular/src/app/shared/config/rule-list/rule-list.ts @@ -0,0 +1,226 @@ +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, HostBinding, HostListener, Input, QueryList, ViewChildren } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { SfngDialogService } from '@safing/ui'; +import { RuleListItemComponent } from './list-item'; + +@Component({ + selector: 'app-rule-list', + templateUrl: './rule-list.html', + styleUrls: ['./rule-list.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RuleListComponent), + multi: true, + } + ], +}) +export class RuleListComponent implements ControlValueAccessor { + /** Add the host element into the tab-sequence */ + @HostBinding('tabindex') + readonly tabindex = 0; + + @ViewChildren(RuleListItemComponent) + renderedRules!: QueryList; + + /** A list of selected rule indexes */ + selectedItems: number[] = []; + + /** + * @private + * Mark the component as dirty by calling the onTouch callback of the control-value accessor + */ + @HostListener('blur') + onBlur() { + this.onTouch(); + } + + @Input() + symbolMap = { + '+': 'Allow', + '-': 'Block', + } + + /** + * Whether or not the component should be displayed as read-only. + */ + @Input() + set readonly(v: any) { + this._readonly = coerceBooleanProperty(v); + } + get readonly() { + return this._readonly; + } + private _readonly = false; + + /** + * @private + * The actual rule entries. Displayed as RuleListItemComponent. + */ + entries: string[] = []; + + constructor( + private changeDetector: ChangeDetectorRef, + private dialog: SfngDialogService + ) { } + + /** + * @private + * Update the value of a rule-list entry. Used as a callback function + * for the valueChange output of the RuleListItemComponent. + * + * @param index The index of the rule list entry to update + * @param newValue The new value of the rule + */ + updateValue(index: number, newValue: string) { + // we need create a copy of the actual value as + // the parent component might still have a reference + // to the current values. + this.entries = [ + ...this.entries, + ]; + this.entries[index] = newValue; + + // tell the control that we have a new value + this.onChange(this.entries); + } + + /** + * @private + * Delete a rule list entry. + * + * @param index The index of the rule list entry to delete + */ + deleteEntry(index: number) { + this.entries = [...this.entries]; + this.entries.splice(index, 1); + this.onChange(this.entries); + } + + /** + * @private + * Add a new, empty rule list entry at the end of the + * list. + * + * This is a no-op if there's already an empty item + * available. + */ + addEntry() { + // if there's already one empty entry abort + if (this.entries.some(e => e.trim() === '')) { + return; + } + + this.entries = [...this.entries]; + this.entries.push(''); + } + + /** + * Set a new value for the rule list. This is the + * only way to configure the existing entries and is + * used by the control-value-accessor and ngModel. + * + * @param value The new value set via [ngModel] + */ + writeValue(value: string[]) { + this.entries = value; + + this.changeDetector.markForCheck(); + } + + /** Toggles selection of a rule item */ + selectItem(index: number, selected: boolean) { + if (selected && !this.selectedItems.includes(index)) { + this.selectedItems = [ + ...this.selectedItems, + index, + ] + + return; + } + + if (!selected && this.selectedItems.includes(index)) { + this.selectedItems = this.selectedItems.filter(idx => idx !== index) + + return; + } + } + + /** Removes all selected items after displaying a confirmation dialog. */ + removeSelectedItems() { + this.dialog.confirm({ + buttons: [ + { + id: 'abort', + text: 'Cancel', + class: 'outline' + }, + { + id: 'delete', + text: 'Delete Rules', + class: 'danger' + } + ], + canCancel: true, + caption: 'Caution', + header: 'Rule Deletion', + message: 'Do you want to delete the selected rules' + }) + .onAction('delete', () => { + this.entries = this.entries.filter((_, idx: number) => !this.selectedItems.includes(idx)) + this.abortSelection(); + this.onChange(this.entries); + }) + + } + + /** Aborts the current selection */ + abortSelection() { + this.selectedItems.forEach(itemIdx => this.renderedRules.get(itemIdx)?.toggleSelection()) + this.selectedItems = []; + } + + /** @private onChange callback registered by ngModel and form controls */ + onChange = (_: string[]): void => { }; + + /** Registers the onChange callback and required for the ControlValueAccessor interface */ + registerOnChange(fn: (value: string[]) => void) { + this.onChange = fn; + } + + /** @private onTouch callback registered by ngModel and form controls */ + onTouch = (): void => { }; + + /** Registers the onChange callback and required for the ControlValueAccessor interface */ + registerOnTouched(fn: () => void) { + this.onTouch = fn; + } + + /** + * @private + * Used as a callback for the @angular/cdk drop component + * and used to update the actual order of the entries. + * + * @param event The drop-event + */ + drop(event: CdkDragDrop) { + if (this._readonly) { + return; + } + + // create a copy of the array + this.entries = [...this.entries]; + moveItemInArray(this.entries, event.previousIndex, event.currentIndex); + + this.changeDetector.markForCheck(); + this.onChange(this.entries); + } + + /** @private TrackByFunction for entries */ + trackBy(idx: number, value: string) { + return `${value}`; + } +} diff --git a/desktop/angular/src/app/shared/config/safe.pipe.ts b/desktop/angular/src/app/shared/config/safe.pipe.ts new file mode 100644 index 00000000..0cbf2855 --- /dev/null +++ b/desktop/angular/src/app/shared/config/safe.pipe.ts @@ -0,0 +1,21 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DomSanitizer, SafeHtml, SafeStyle, SafeScript, SafeUrl, SafeResourceUrl } from '@angular/platform-browser'; + +@Pipe({ + name: 'safe' +}) +export class SafePipe implements PipeTransform { + + constructor(protected sanitizer: DomSanitizer) { } + + public transform(value: any, type: string): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl { + switch (type) { + case 'html': return this.sanitizer.bypassSecurityTrustHtml(value); + case 'style': return this.sanitizer.bypassSecurityTrustStyle(value); + case 'script': return this.sanitizer.bypassSecurityTrustScript(value); + case 'url': return this.sanitizer.bypassSecurityTrustUrl(value); + case 'resourceUrl': return this.sanitizer.bypassSecurityTrustResourceUrl(value); + default: throw new Error(`Invalid safe type specified: ${type}`); + } + } +} diff --git a/desktop/angular/src/app/shared/count-indicator/count-indicator.html b/desktop/angular/src/app/shared/count-indicator/count-indicator.html new file mode 100644 index 00000000..fdbb0c22 --- /dev/null +++ b/desktop/angular/src/app/shared/count-indicator/count-indicator.html @@ -0,0 +1,4 @@ +{{ count | prettyCount }} +
+
+
diff --git a/desktop/angular/src/app/shared/count-indicator/count-indicator.module.ts b/desktop/angular/src/app/shared/count-indicator/count-indicator.module.ts new file mode 100644 index 00000000..0961a129 --- /dev/null +++ b/desktop/angular/src/app/shared/count-indicator/count-indicator.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from "@angular/core"; +import { CountIndicatorComponent } from "./count-indicator"; +import { PrettyCountPipe } from "./count.pipe"; + +@NgModule({ + declarations: [ + CountIndicatorComponent, + PrettyCountPipe, + ], + exports: [ + CountIndicatorComponent, + PrettyCountPipe, + ] +}) +export class CountIndicatorModule { } diff --git a/desktop/angular/src/app/shared/count-indicator/count-indicator.scss b/desktop/angular/src/app/shared/count-indicator/count-indicator.scss new file mode 100644 index 00000000..3d97d2c9 --- /dev/null +++ b/desktop/angular/src/app/shared/count-indicator/count-indicator.scss @@ -0,0 +1,8 @@ +@import '../../../theme/mixins/_pill.scss'; + +:host { + @include pill-container; + @apply pl-2; + @apply bg-buttons-dark; + @apply w-20; +} diff --git a/desktop/angular/src/app/shared/count-indicator/count-indicator.ts b/desktop/angular/src/app/shared/count-indicator/count-indicator.ts new file mode 100644 index 00000000..8c49e098 --- /dev/null +++ b/desktop/angular/src/app/shared/count-indicator/count-indicator.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; + +@Component({ + selector: 'app-count-indicator', + templateUrl: './count-indicator.html', + styleUrls: ['./count-indicator.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CountIndicatorComponent implements OnChanges { + @Input() + count = 0; + + @Input() + countAllowed: number = 0; + + allowedPercentage: number = 0; + + ngOnChanges() { + const ratio = (this.countAllowed / this.count) || 0; + this.allowedPercentage = Math.round(ratio * 100); + } +} diff --git a/desktop/angular/src/app/shared/count-indicator/count.pipe.ts b/desktop/angular/src/app/shared/count-indicator/count.pipe.ts new file mode 100644 index 00000000..69140b3e --- /dev/null +++ b/desktop/angular/src/app/shared/count-indicator/count.pipe.ts @@ -0,0 +1,18 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: 'prettyCount', + pure: true +}) +export class PrettyCountPipe implements PipeTransform { + transform(value: number) { + if (value > 999) { + const v = Math.floor(value / 1000); + if (value === v * 1000) { + return `${v}k`; + } + return `${v}k+` + } + return `${value}` + } +} diff --git a/desktop/angular/src/app/shared/count-indicator/index.ts b/desktop/angular/src/app/shared/count-indicator/index.ts new file mode 100644 index 00000000..be4276b7 --- /dev/null +++ b/desktop/angular/src/app/shared/count-indicator/index.ts @@ -0,0 +1,2 @@ +export * from './count-indicator'; +export * from './count-indicator.module'; diff --git a/desktop/angular/src/app/shared/country-flag/country-flag.ts b/desktop/angular/src/app/shared/country-flag/country-flag.ts new file mode 100644 index 00000000..df91a3c5 --- /dev/null +++ b/desktop/angular/src/app/shared/country-flag/country-flag.ts @@ -0,0 +1,45 @@ +import { AfterViewInit, Directive, ElementRef, HostBinding, Input, OnChanges, Renderer2, SimpleChanges } from '@angular/core'; + +@Directive({ + selector: 'span[appCountryFlags]', +}) +export class CountryFlagDirective implements AfterViewInit, OnChanges { + private readonly flagDir = "/assets/img/flags/"; + private readonly OFFSET = 127397; + + @HostBinding('style.text-shadow') + textShadow = 'rgba(255, 255, 255, .5) 0px 0px 1px'; + + @Input() + appCountryFlags: string = ''; + + constructor( + private el: ElementRef, + private renderer: Renderer2 + ) { } + + ngOnChanges(changes: SimpleChanges): void { + if (!changes['appCountryFlags'].isFirstChange()) { + this.update(); + } + } + + ngAfterViewInit() { + this.update(); + } + + private update() { + const span = this.el.nativeElement as HTMLSpanElement; + const flag = this.toUnicodeFlag(this.appCountryFlags); + this.renderer.setAttribute(span, 'data-before', flag); + + span.innerHTML = ``; + } + + private toUnicodeFlag(code: string) { + const base = 127462 - 65; + const cc = code.toUpperCase(); + const res = String.fromCodePoint(...cc.split('').map(c => base + c.charCodeAt(0))); + return res; + } +} diff --git a/desktop/angular/src/app/shared/country-flag/country.module.ts b/desktop/angular/src/app/shared/country-flag/country.module.ts new file mode 100644 index 00000000..2acdb3f8 --- /dev/null +++ b/desktop/angular/src/app/shared/country-flag/country.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CountryFlagDirective } from './country-flag'; + +@NgModule({ + declarations: [ + CountryFlagDirective + ], + exports: [ + CountryFlagDirective, + ] +}) +export class CountryFlagModule { } diff --git a/desktop/angular/src/app/shared/country-flag/index.ts b/desktop/angular/src/app/shared/country-flag/index.ts new file mode 100644 index 00000000..cc7d4306 --- /dev/null +++ b/desktop/angular/src/app/shared/country-flag/index.ts @@ -0,0 +1,2 @@ +export * from './country-flag'; +export * from './country.module'; diff --git a/desktop/angular/src/app/shared/edit-profile-dialog/edit-profile-dialog.html b/desktop/angular/src/app/shared/edit-profile-dialog/edit-profile-dialog.html new file mode 100644 index 00000000..7b630621 --- /dev/null +++ b/desktop/angular/src/app/shared/edit-profile-dialog/edit-profile-dialog.html @@ -0,0 +1,322 @@ +

+ + + {{ isEditMode ? 'Edit App Profile' : 'Create New App Profile' }} +

+ +
+ + +
+ + Configure basic profile information like the profile name, it's + description and optionally the profile icon. + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+
+ + +
+ The icon must be smaller than 10kB and it's dimensions must not + exceed 512x512 px. Only JPG and PNG files are supported. +
+ + {{ imageError }} +
+ + +
+
+
+ + +
+ This profile will be applied to processes that match one of the + following fingerprints: + +
+ No fingerprints configured. Please press "Add New" to get started. +
+
+
+ + + + + + + + + Tag + + + Command Line + + Environment + + Path + + + + + + + {{ tag.Name }} + + + + + Equals + Prefix + Regex + + + + + +
+
+ + +
+
+ + +
+
+ Select a Profile to copy settings from: +
+ + + + + {{ p.Name }} + + + + +
+ +
+
+ + + + + {{ p.Name }} +
+ +
+
+ + Settings will be copied from all specified profiles in order with + settings from higher profiles taking precedence.
+ Existing settings may be overwritten. +
+
+
+
+
+ +
+ +
+ + +
diff --git a/desktop/angular/src/app/shared/edit-profile-dialog/edit-profile-dialog.scss b/desktop/angular/src/app/shared/edit-profile-dialog/edit-profile-dialog.scss new file mode 100644 index 00000000..03e9ccea --- /dev/null +++ b/desktop/angular/src/app/shared/edit-profile-dialog/edit-profile-dialog.scss @@ -0,0 +1,29 @@ +:host { + @apply flex flex-col gap-4 max-w-2xl; + min-width: 500px; + width: 60vw; +} + +.tab-content { + @apply flex flex-col gap-4 overflow-x-hidden h-96 pt-2; +} + +.input { + @apply flex flex-col gap-1; + + label { + @apply text-primary uppercase text-xxs relative left-1.5; + } + + input[type="text"] { + @apply border border-gray-500; + + &.ng-invalid.ng-dirty { + @apply border-red-200; + } + } + + input[type="file"] { + display: none; + } +} diff --git a/desktop/angular/src/app/shared/edit-profile-dialog/edit-profile-dialog.ts b/desktop/angular/src/app/shared/edit-profile-dialog/edit-profile-dialog.ts new file mode 100644 index 00000000..60b5b514 --- /dev/null +++ b/desktop/angular/src/app/shared/edit-profile-dialog/edit-profile-dialog.ts @@ -0,0 +1,393 @@ +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { + ChangeDetectorRef, + Component, + Inject, + OnDestroy, + OnInit, + TrackByFunction, +} from '@angular/core'; +import { + AppProfile, + AppProfileService, + FingerpringOperation, + Fingerprint, + FingerprintType, + PORTMASTER_HTTP_API_ENDPOINT, + PortapiService, + Record, + TagDescription, + mergeDeep, +} from '@safing/portmaster-api'; +import { SFNG_DIALOG_REF, SfngDialogRef, SfngDialogService } from '@safing/ui'; +import { Observable, Subject, map, of, switchMap, takeUntil } from 'rxjs'; +import { ActionIndicatorService } from 'src/app/shared/action-indicator'; + +@Component({ + templateUrl: './edit-profile-dialog.html', + //changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./edit-profile-dialog.scss'], +}) +// eslint-disable-next-line @angular-eslint/component-class-suffix +export class EditProfileDialog implements OnInit, OnDestroy { + private destory$ = new Subject(); + + profile: Partial = { + ID: '', + Source: 'local', + Name: '', + Description: '', + Icons: [], + Fingerprints: [], + }; + + isEditMode = false; + iconData: string | ArrayBuffer = ''; + iconType: string = ''; + iconChanged = false; + iconObjectURL = ''; + imageError: string | null = null; + + allProfiles: AppProfile[] = []; + + copySettingsFrom: AppProfile[] = []; + + selectedCopyFrom: AppProfile | null = null; + + fingerPrintTypes = FingerprintType; + fingerPrintOperations = FingerpringOperation; + processTags: TagDescription[] = []; + + trackFingerPrint: TrackByFunction = ( + _: number, + fp: Fingerprint + ) => `${fp.Type}-${fp.Key}-${fp.Operation}-${fp.Value}`; + + constructor( + @Inject(SFNG_DIALOG_REF) + private dialgoRef: SfngDialogRef< + EditProfileDialog, + any, + string | null | AppProfile + >, + private profileService: AppProfileService, + private portapi: PortapiService, + private actionIndicator: ActionIndicatorService, + private dialog: SfngDialogService, + private cdr: ChangeDetectorRef, + @Inject(PORTMASTER_HTTP_API_ENDPOINT) private httpAPI: string + ) { } + + ngOnInit(): void { + this.profileService.tagDescriptions().subscribe((result) => { + this.processTags = result; + this.cdr.markForCheck(); + }); + + this.profileService + .watchProfiles() + .pipe(takeUntil(this.destory$)) + .subscribe((profiles) => { + this.allProfiles = profiles; + this.cdr.markForCheck(); + }); + + if (!!this.dialgoRef.data && typeof this.dialgoRef.data === 'string') { + this.isEditMode = true; + this.profileService + .getAppProfile(this.dialgoRef.data) + .subscribe((profile) => { + this.profile = profile; + this.loadIcon(); + }); + } else if ( + !!this.dialgoRef.data && + typeof this.dialgoRef.data === 'object' + ) { + this.profile = this.dialgoRef.data; + this.loadIcon(); + } + } + + private loadIcon() { + if (!this.profile.Icons?.length) { + return; + } + + const firstIcon = this.profile.Icons[0]; + + // get the current icon of the profile + switch (firstIcon.Type) { + case 'database': + this.portapi + .get(firstIcon.Value) + .subscribe((data) => { + this.iconData = data.iconData; + this.iconObjectURL = this.iconData; + this.cdr.markForCheck(); + }); + break; + + case 'api': + this.iconData = `${this.httpAPI}/v1/profile/icon/${firstIcon.Value}`; + this.iconObjectURL = this.iconData; + + break; + + default: + console.error(`Unsupported icon type ${firstIcon.Type}`); + } + + this.cdr.markForCheck(); + } + + ngOnDestroy() { + this.destory$.next(); + this.destory$.complete(); + } + + addFingerprint() { + this.profile.Fingerprints?.push({ + Key: '', + Operation: FingerpringOperation.Equal, + Value: '', + Type: FingerprintType.Path, + }); + } + + removeFingerprint(idx: number) { + this.profile.Fingerprints?.splice(idx, 1); + this.profile.Fingerprints = [...this.profile.Fingerprints!]; + } + + removeCopyFrom(idx: number) { + this.copySettingsFrom.splice(idx, 1); + this.copySettingsFrom = [...this.copySettingsFrom]; + } + + addCopyFrom() { + this.copySettingsFrom = [...this.copySettingsFrom, this.selectedCopyFrom!]; + this.selectedCopyFrom = null; + } + + drop(event: CdkDragDrop) { + // create a copy of the array + this.copySettingsFrom = [...this.copySettingsFrom]; + moveItemInArray( + this.copySettingsFrom, + event.previousIndex, + event.currentIndex + ); + + this.cdr.markForCheck(); + } + + deleteProfile() { + this.dialog + .confirm({ + caption: 'Caution', + header: 'Confirm Profile Deletion', + message: 'Do you want to delete this profile?', + buttons: [ + { + id: 'delete', + class: 'danger', + text: 'Delete', + }, + { + id: 'abort', + class: 'outline', + text: 'Cancel', + }, + ], + }) + .onAction('delete', () => { + this.profileService + .deleteProfile(this.profile as AppProfile) + .subscribe({ + next: () => this.dialgoRef.close('deleted'), + error: (err) => { + this.actionIndicator.error('Failed to delete profile', err); + }, + }); + }); + } + + resetIcon() { + this.iconChanged = true; + this.iconData = ''; + this.iconType = ''; + this.iconObjectURL = ''; + } + + save() { + if (!this.profile.ID) { + this.profile.ID = this.uuidv4(); + } + + if (!this.profile.Source) { + this.profile.Source = 'local'; + } + + let updateIcon: Observable = of(undefined); + + if (this.iconChanged) { + // delete any previously set icon + this.profile.Icons?.forEach((icon) => { + if (icon.Type === 'database') { + this.portapi.delete(icon.Value).subscribe(); + } + + // FIXME(ppacher): we cannot yet delete API based icons ... + }); + + if (this.iconData !== '') { + // save the new icon in the cache database + + // FIXME(ppacher): we currently need to calls because the icon API in portmaster + // does not update the profile but just saves the file and returns the filename. + // So we still need to update the profile manually. + updateIcon = this.profileService + .setProfileIcon(this.iconData, this.iconType) + .pipe( + map(({ filename }) => { + this.profile.Icons = [ + { + Type: 'api', + Value: filename, + Source: 'user', + }, + ]; + }) + ); + + // FIXME(ppacher): reset presentationpath + } else { + // just clear out that there was an icon + this.profile.Icons = []; + } + } + + if (this.profile.Fingerprints!.length > 1) { + this.profile.PresentationPath = ''; + } + const oldConfig = this.profile.Config || {}; + this.profile.Config = {}; + + mergeDeep( + this.profile.Config, + ...[...this.copySettingsFrom.map((p) => p.Config || {}), oldConfig] + ); + + updateIcon + .pipe( + switchMap(() => { + return this.profileService.saveProfile(this.profile as AppProfile); + }) + ) + .subscribe({ + next: () => { + this.actionIndicator.success( + this.profile.Name!, + 'Profile saved successfully' + ); + this.dialgoRef.close('saved'); + }, + error: (err) => { + this.actionIndicator.error('Failed to save profile', err); + }, + }); + } + + abort() { + this.dialgoRef.close('abort'); + } + + fileChangeEvent(fileInput: any) { + this.imageError = null; + this.iconData = ''; + this.iconChanged = true; + + if (fileInput.target.files && fileInput.target.files[0]) { + const max_size = 10 * 1024; + const allowed_types = [ + 'image/png', + 'image/jpeg', + 'image/svg', + 'image/gif', + 'image/tiff', + ]; + const max_height = 512; + const max_width = 512; + const file: File = fileInput.target.files[0]; + + if (file.size > max_size) { + this.imageError = 'Maximum size allowed is ' + max_size / 1000 + 'KB'; + } + + if (!allowed_types.includes(file.type)) { + this.imageError = 'Only JPG, PNG, SVG, GIF or Tiff files are allowed'; + } + + this.iconType = file.type; + + const reader = new FileReader(); + reader.onload = (e: ProgressEvent) => { + const content: ArrayBuffer = e.target!.result! as ArrayBuffer; + const blob = new Blob([content], { type: file.type }); + + const image = new Image(); + image.src = URL.createObjectURL(blob); + this.iconObjectURL = image.src; + + image.onload = (rs: any) => { + const img_height = rs.currentTarget['height']!; + const img_width = rs.currentTarget['width']; + + if (img_height > max_height && img_width > max_width) { + this.imageError = + 'Maximum dimentions allowed ' + + max_height + + '*' + + max_width + + 'px'; + } else { + this.iconData = content; + } + + this.cdr.markForCheck(); + }; + + image.onerror = (err: any) => { + this.actionIndicator.error( + 'Failed to get image', + this.actionIndicator.getErrorMessgae(err) + ); + }; + + this.cdr.markForCheck(); + }; + + reader.onerror = (err: any) => { + this.actionIndicator.error( + 'Failed to get image', + this.actionIndicator.getErrorMessgae(err) + ); + }; + + reader.readAsArrayBuffer(fileInput.target.files[0]); + } + } + + private uuidv4(): string { + if (typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + + // This one is not really random and not RFC compliant but serves enough for fallback + // purposes if the UI is opened in a browser that does not yet support randomUUID + console.warn('Using browser with lacking support for crypto.randomUUID()'); + + return Date.now().toString(36) + Math.random().toString(36).substring(2); + } +} diff --git a/desktop/angular/src/app/shared/edit-profile-dialog/index.ts b/desktop/angular/src/app/shared/edit-profile-dialog/index.ts new file mode 100644 index 00000000..0a4c617d --- /dev/null +++ b/desktop/angular/src/app/shared/edit-profile-dialog/index.ts @@ -0,0 +1 @@ +export * from './edit-profile-dialog'; diff --git a/desktop/angular/src/app/shared/exit-screen/exit-screen.html b/desktop/angular/src/app/shared/exit-screen/exit-screen.html new file mode 100644 index 00000000..cff2a960 --- /dev/null +++ b/desktop/angular/src/app/shared/exit-screen/exit-screen.html @@ -0,0 +1,19 @@ +
+ Tip + + +

Close User Interface

+ + Closing the User Interface does not shut down the Portmaster. You can shut down the Portmaster + in the Settings or the Tray Notifier. + +
+ + + + + +
+
diff --git a/desktop/angular/src/app/shared/exit-screen/exit-screen.scss b/desktop/angular/src/app/shared/exit-screen/exit-screen.scss new file mode 100644 index 00000000..65e5dc8b --- /dev/null +++ b/desktop/angular/src/app/shared/exit-screen/exit-screen.scss @@ -0,0 +1,68 @@ +caption { + @apply text-sm; + opacity : .6; + font-size: .6rem; +} + +.content-wrapper { + display : flex; + flex-direction: column; + align-items : flex-start; + + h1 { + font-size : 0.85rem; + font-weight : 500; + margin-bottom: 1rem; + } + + .message, + h1 { + flex-shrink : 0; + text-overflow: ellipsis; + word-break : normal; + } + + .message { + font-size: 0.75rem; + flex-grow: 1; + opacity : .6; + } + + .close-icon { + position: absolute; + top : 1rem; + right : 1rem; + opacity : .7; + cursor : pointer; + + &:hover { + opacity: 1; + } + } + + .actions { + margin-top : 1rem; + width : 100%; + display : flex; + justify-content: space-between; + align-items : center; + + button { + @apply bg-info-blue; + + &.danger { + @apply bg-info-red; + } + } + + &>span { + display : flex; + align-items: center; + + label { + margin-left: .5rem; + user-select: none; + } + } + } +} diff --git a/desktop/angular/src/app/shared/exit-screen/exit-screen.ts b/desktop/angular/src/app/shared/exit-screen/exit-screen.ts new file mode 100644 index 00000000..3dbda654 --- /dev/null +++ b/desktop/angular/src/app/shared/exit-screen/exit-screen.ts @@ -0,0 +1,52 @@ +import { OverlayRef } from '@angular/cdk/overlay'; +import { Component, Inject, InjectionToken } from '@angular/core'; +import { SfngDialogRef, SFNG_DIALOG_REF } from '@safing/ui'; +import { Observable, of } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; +import { UIStateService } from 'src/app/services'; +import { fadeInAnimation, fadeOutAnimation } from '../animations'; + +export const OVERLAYREF = new InjectionToken('OverlayRef'); + +@Component({ + templateUrl: './exit-screen.html', + styleUrls: ['./exit-screen.scss'], + animations: [ + fadeInAnimation, + fadeOutAnimation, + ] +}) +export class ExitScreenComponent { + constructor( + @Inject(SFNG_DIALOG_REF) private _dialogRef: SfngDialogRef, + private stateService: UIStateService, + ) { } + + /** @private - used as ngModel form the template */ + neveragain: boolean = false; + + closeUI() { + const closeObserver = { + next: () => { + this._dialogRef.close('exit'); + } + } + + let close: Observable = of(null); + if (this.neveragain) { + close = this.stateService.uiState() + .pipe( + map(state => { + state.hideExitScreen = true; + return state; + }), + switchMap(state => this.stateService.saveState(state)), + ) + } + close.subscribe(closeObserver) + } + + cancel() { + this._dialogRef.close() + } +} diff --git a/desktop/angular/src/app/shared/exit-screen/exit.service.ts b/desktop/angular/src/app/shared/exit-screen/exit.service.ts new file mode 100644 index 00000000..c1da5b92 --- /dev/null +++ b/desktop/angular/src/app/shared/exit-screen/exit.service.ts @@ -0,0 +1,146 @@ +import { IntegrationService } from './../../integration/integration'; +import { Injectable, inject } from '@angular/core'; +import { PortapiService } from '@safing/portmaster-api'; +import { SfngDialogService } from '@safing/ui'; +import { BehaviorSubject, merge, of } from 'rxjs'; +import { catchError, debounceTime, distinctUntilChanged, map, skip, switchMap, tap, timeout } from 'rxjs/operators'; +import { UIStateService } from 'src/app/services'; +import { ActionIndicatorService } from '../action-indicator'; +import { ExitScreenComponent } from './exit-screen'; +import { INTEGRATION_SERVICE } from 'src/app/integration'; + +const MessageConnecting = 'Connecting to Portmaster'; +const MessageShutdown = 'Shutting Down Portmaster'; +const MessageRestart = 'Restarting Portmaster'; +const MessageHidden = ''; + +export type OverlayMessage = typeof MessageConnecting + | typeof MessageShutdown + | typeof MessageRestart + | typeof MessageHidden; + +@Injectable({ providedIn: 'root' }) +export class ExitService { + private integration = inject(INTEGRATION_SERVICE); + + private hasOverlay = false; + + private _showOverlay = new BehaviorSubject(MessageConnecting); + + /** + * Emits whenever the "Connecting to ..." or "Restarting ..." overlays + * should be shown. It actually emits the message that should be shown. + * An empty string indicates the overlay should be closed. + */ + get showOverlay$() { return this._showOverlay.asObservable() } + + constructor( + private stateService: UIStateService, + private portapi: PortapiService, + private dialog: SfngDialogService, + private uai: ActionIndicatorService, + ) { + + this.portapi.connected$ + .pipe( + distinctUntilChanged(), + ) + .subscribe(connected => { + if (connected) { + this._showOverlay.next(MessageHidden); + } else if (this._showOverlay.getValue() !== MessageShutdown) { + this._showOverlay.next(MessageConnecting) + } + }) + + + let restartInProgress = false; + merge( + this.portapi.sub('runtime:modules/core/event/shutdown') + .pipe(map(() => MessageShutdown)), + this.portapi.sub('runtime:modules/core/event/restart') + .pipe( + tap(() => restartInProgress = true), + map(() => MessageRestart) + ), + ) + .pipe( + tap(msg => this._showOverlay.next(msg)), + switchMap(() => this.portapi.connected$), + distinctUntilChanged(), + skip(1), + debounceTime(1000), // make sure we display the "shutdown" overlay for at least a second + ) + .subscribe(connected => { + if (this._showOverlay.getValue() === MessageShutdown) { + setTimeout(() => { + this.integration.exitApp(); + }, 1000) + } + + if (connected && restartInProgress) { + restartInProgress = false; + this.portapi.reloadUI() + .pipe( + tap(() => { + setTimeout(() => window.location.reload(), 1000) + }) + ) + .subscribe(this.uai.httpObserver( + 'Reloading UI ...', + 'Failed to Reload UI', + )) + } + }) + + window.addEventListener('beforeunload', () => { + // best effort. may not work all the time depending on + // the current websocket buffer state + this.portapi.bridgeAPI('ui/reload', 'POST').subscribe(); + }) + + this.integration.onExitRequest(() => { + this.stateService.uiState() + // make sure to not wait for the portmaster to start + .pipe(timeout(1000), catchError(() => of(null))) + .subscribe(state => { + if (state?.hideExitScreen) { + this.integration.exitApp(); + return + } + + if (this.hasOverlay) { + return; + } + this.hasOverlay = true; + + this.dialog.create(ExitScreenComponent, { autoclose: true }) + .onAction('exit', () => this.integration.exitApp()) + .onClose.subscribe(() => this.hasOverlay = false); + }) + }) + } + + shutdownPortmaster() { + this.dialog.confirm({ + canCancel: true, + header: 'Shutting Down Portmaster', + message: 'Shutting down the Portmaster will stop all Portmaster components and will leave your system unprotected!', + caption: 'Caution', + buttons: [ + { + id: 'shutdown', + class: 'danger', + text: 'Shut Down Portmaster' + } + ] + }) + .onAction('shutdown', () => { + this.portapi.shutdownPortmaster() + .subscribe(this.uai.httpObserver( + 'Shutting Down ...', + 'Failed to Shut Down', + )) + }) + } +} diff --git a/desktop/angular/src/app/shared/exit-screen/index.ts b/desktop/angular/src/app/shared/exit-screen/index.ts new file mode 100644 index 00000000..8ea5634d --- /dev/null +++ b/desktop/angular/src/app/shared/exit-screen/index.ts @@ -0,0 +1,2 @@ +export * from './exit.service'; +export * from './exit-screen'; diff --git a/desktop/angular/src/app/shared/expertise/expertise-directive.ts b/desktop/angular/src/app/shared/expertise/expertise-directive.ts new file mode 100644 index 00000000..379466af --- /dev/null +++ b/desktop/angular/src/app/shared/expertise/expertise-directive.ts @@ -0,0 +1,93 @@ +import { Directive, EmbeddedViewRef, Input, isDevMode, OnDestroy, OnInit, TemplateRef, ViewContainerRef } from '@angular/core'; +import { ExpertiseLevelNumber } from '@safing/portmaster-api'; +import { Subscription } from 'rxjs'; +import { ExpertiseService } from './expertise.service'; + +// ExpertiseLevelOverwrite may be called to display a DOM node decorated +// with [appExpertiseLevel] even if the current user setting does not +// match the required expertise. +export type ExpertiseLevelOverwrite = (lvl: ExpertiseLevelNumber, data: T) => boolean; +@Directive({ + selector: '[appExpertiseLevel]', +}) +export class ExpertiseDirective implements OnInit, OnDestroy { + private allowedValue: ExpertiseLevelNumber = ExpertiseLevelNumber.user; + private subscription = Subscription.EMPTY; + private view: EmbeddedViewRef | null = null; + + @Input() + set appExpertiseLevelOverwrite(fn: ExpertiseLevelOverwrite) { + this._levelOverwriteFn = fn; + this.update(); + } + private _levelOverwriteFn: ExpertiseLevelOverwrite | null = null; + + @Input() + set appExpertiseLevelData(d: T) { + this._data = d; + this.update(); + } + private _data: T | undefined = undefined; + + @Input() + set appExpertiseLevel(lvl: ExpertiseLevelNumber | string) { + if (typeof lvl === 'string') { + lvl = ExpertiseLevelNumber[lvl as any]; + } + if (lvl === undefined) { + if (isDevMode()) { + throw new Error(`[appExpertiseLevel] got undefined expertise-level value`); + } + return; + } + if (lvl !== this.allowedValue) { + this.allowedValue = lvl as ExpertiseLevelNumber; + this.update(); + } + } + + private update() { + const current = ExpertiseLevelNumber[this.expertiseService.currentLevel]; + let hide = current < this.allowedValue; + + // if there's an overwrite function defined make sue to check that. + if (hide && !!this._levelOverwriteFn) { + hide = !this._levelOverwriteFn(current, this._data!); + if (!hide) { + console.log("overwritten", current, this._data); + } + } + + if (hide) { + if (!!this.view) { + this.view.destroy(); + this.viewContainer.clear(); + this.view = null; + } + return + } + + if (!!this.view) { + this.view.markForCheck(); + return; + } + + this.view = this.viewContainer.createEmbeddedView(this.templateRef); + this.view.detectChanges(); + } + + constructor( + private expertiseService: ExpertiseService, + private templateRef: TemplateRef, + private viewContainer: ViewContainerRef + ) { } + + ngOnInit() { + this.subscription = this.expertiseService.change.subscribe(() => this.update()) + } + + ngOnDestroy() { + this.viewContainer.clear(); + this.subscription.unsubscribe(); + } +} diff --git a/desktop/angular/src/app/shared/expertise/expertise-switch.html b/desktop/angular/src/app/shared/expertise/expertise-switch.html new file mode 100644 index 00000000..8ad8eca7 --- /dev/null +++ b/desktop/angular/src/app/shared/expertise/expertise-switch.html @@ -0,0 +1,16 @@ + + + + Simple Interface + + + + Advanced Interface + + + + + Developer Interface + + + diff --git a/desktop/angular/src/app/shared/expertise/expertise-switch.scss b/desktop/angular/src/app/shared/expertise/expertise-switch.scss new file mode 100644 index 00000000..b795503d --- /dev/null +++ b/desktop/angular/src/app/shared/expertise/expertise-switch.scss @@ -0,0 +1,12 @@ +:host { + display: flex; + @apply pl-2; + user-select: none; + flex-direction: row; + align-items: center; + justify-content: center; +} + +sfng-tipup { + margin-right: 0.5rem; +} diff --git a/desktop/angular/src/app/shared/expertise/expertise-switch.ts b/desktop/angular/src/app/shared/expertise/expertise-switch.ts new file mode 100644 index 00000000..1b822927 --- /dev/null +++ b/desktop/angular/src/app/shared/expertise/expertise-switch.ts @@ -0,0 +1,38 @@ +import { Component, ElementRef } from '@angular/core'; +import { ExpertiseLevel } from '@safing/portmaster-api'; +import { ExpertiseService } from './expertise.service'; + +@Component({ + selector: 'app-expertise', + templateUrl: './expertise-switch.html', + styleUrls: ['./expertise-switch.scss'] +}) +export class ExpertiseComponent { + /** @private provide the expertise-level enums to the template */ + readonly expertiseLevels = ExpertiseLevel; + + currentLevel = this.expertiseService.change; + + /** + * @private + * Getter to access the expertise level as saved in the database + */ + get savedLevel() { + return this.expertiseService.savedLevel; + } + + constructor( + private expertiseService: ExpertiseService, + public host: ElementRef, + ) { } + + /** + * @private + * Configures a new expertise level + * + * @param lvl The new expertise level to use + */ + selectLevel(lvl: ExpertiseLevel) { + this.expertiseService.setLevel(lvl); + } +} diff --git a/desktop/angular/src/app/shared/expertise/expertise.module.ts b/desktop/angular/src/app/shared/expertise/expertise.module.ts new file mode 100644 index 00000000..7bf6a7fa --- /dev/null +++ b/desktop/angular/src/app/shared/expertise/expertise.module.ts @@ -0,0 +1,24 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { SfngSelectModule, SfngTipUpModule } from "@safing/ui"; +import { ExpertiseDirective } from "./expertise-directive"; +import { ExpertiseComponent } from "./expertise-switch"; + +@NgModule({ + imports: [ + SfngSelectModule, + CommonModule, + SfngTipUpModule, + FormsModule, + ], + declarations: [ + ExpertiseComponent, + ExpertiseDirective, + ], + exports: [ + ExpertiseComponent, + ExpertiseDirective, + ] +}) +export class ExpertiseModule { } diff --git a/desktop/angular/src/app/shared/expertise/expertise.service.ts b/desktop/angular/src/app/shared/expertise/expertise.service.ts new file mode 100644 index 00000000..5b5d7a20 --- /dev/null +++ b/desktop/angular/src/app/shared/expertise/expertise.service.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@angular/core'; +import { ConfigService, ExpertiseLevel, StringSetting } from '@safing/portmaster-api'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { distinctUntilChanged, map, repeat, share } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class ExpertiseService { + /** If the user overwrites the expertise level on a per-page setting we track that here */ + private _localOverwrite: ExpertiseLevel | null = null; + private _currentLevel: ExpertiseLevel = ExpertiseLevel.User; + + /** Watches the expertise level as saved in the configuration */ + private _savedLevel$ = this.configService.watch('core/expertiseLevel') + .pipe( + repeat({ delay: 2000 }), + map(upd => { + return upd as ExpertiseLevel; + }), + distinctUntilChanged(), + share(), + ); + + private level$ = new BehaviorSubject(ExpertiseLevel.User); + + get currentLevel() { + return this._localOverwrite === null + ? this._currentLevel + : this._localOverwrite; + } + + get savedLevel() { + return this._currentLevel; + } + + get change(): Observable { + return this.level$.asObservable(); + } + + constructor(private configService: ConfigService) { + this._savedLevel$ + .subscribe(lvl => { + this._currentLevel = lvl; + if (this._localOverwrite === null) { + this.level$.next(lvl); + } + }); + } + + setLevel(lvl: ExpertiseLevel | null) { + if (lvl === this._currentLevel) { + lvl = null; + } + + this._localOverwrite = lvl; + if (!!lvl) { + this.level$.next(lvl); + } else { + this.level$.next(this._currentLevel!); + } + } +} diff --git a/desktop/angular/src/app/shared/expertise/index.ts b/desktop/angular/src/app/shared/expertise/index.ts new file mode 100644 index 00000000..6c41ae61 --- /dev/null +++ b/desktop/angular/src/app/shared/expertise/index.ts @@ -0,0 +1,3 @@ +export * from './expertise-directive'; +export * from './expertise-switch'; +export * from './expertise.service'; diff --git a/desktop/angular/src/app/shared/external-link.directive.ts b/desktop/angular/src/app/shared/external-link.directive.ts new file mode 100644 index 00000000..47a16c28 --- /dev/null +++ b/desktop/angular/src/app/shared/external-link.directive.ts @@ -0,0 +1,53 @@ +import { isPlatformBrowser } from '@angular/common'; +import { + Directive, + HostBinding, HostListener, Inject, + Input, OnChanges, PLATFORM_ID, inject +} from '@angular/core'; +import { INTEGRATION_SERVICE } from '../integration'; + +@Directive({ + // eslint-disable-next-line @angular-eslint/directive-selector + selector: 'a[href]' +}) +export class ExternalLinkDirective implements OnChanges { + private readonly integration = inject(INTEGRATION_SERVICE); + + @HostBinding('attr.rel') + relAttr = ''; + + @HostBinding('attr.target') + targetAttr = ''; + + @HostBinding('attr.href') + hrefAttr = ''; + + @Input() + href: string = ''; + + constructor(@Inject(PLATFORM_ID) private platformId: string) { } + + @HostListener('click', ['$event']) + onClick(event: Event) { + event.preventDefault(); + event.stopPropagation(); + + this.integration.openExternal(this.href); + } + + ngOnChanges() { + this.hrefAttr = this.href; + + if (this.isLinkExternal()) { + this.relAttr = 'noopener'; + this.targetAttr = '_blank'; + } + } + + private isLinkExternal() { + return ( + isPlatformBrowser(this.platformId) && + !this.href.includes(location.hostname) + ); + } +} diff --git a/desktop/angular/src/app/shared/feature-scout/feature-scout.html b/desktop/angular/src/app/shared/feature-scout/feature-scout.html new file mode 100644 index 00000000..f0ee7af7 --- /dev/null +++ b/desktop/angular/src/app/shared/feature-scout/feature-scout.html @@ -0,0 +1,106 @@ + +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+
+ + + + SPN is connecting...
+ Fail-safe blocking enabled +
+ + SPN failed to connect
+ Fail-safe blocking enabled +
+ + SPN is connecting...
+ Fail-safe blocking enabled +
+ + + + + {{ spnStatus?.HomeHubName }} + + in + + + {{ spnStatus?.ConnectedCountry?.Name }} + +
+ +
+
+ + + +
+
+ + + SPN Home (Entry) Node +
    +
  • Connected to {{ spnStatus?.ConnectedIP }}
  • +
  • Uplink is always encrypted
  • +
  • Built with transport/decoy {{ spnStatus?.ConnectedTransport }}
  • +
+
diff --git a/desktop/angular/src/app/shared/feature-scout/feature-scout.scss b/desktop/angular/src/app/shared/feature-scout/feature-scout.scss new file mode 100644 index 00000000..5ae271f6 --- /dev/null +++ b/desktop/angular/src/app/shared/feature-scout/feature-scout.scss @@ -0,0 +1,15 @@ +.feature-icon { + @apply text-primary text-opacity-80; + + &.feature-icon-off { + opacity: 0.25; + } +} + +.status-info { + @apply text-primary text-opacity-80 text-xxs text-center; + + &:hover { + cursor: default; + } +} diff --git a/desktop/angular/src/app/shared/feature-scout/feature-scout.ts b/desktop/angular/src/app/shared/feature-scout/feature-scout.ts new file mode 100644 index 00000000..596edf14 --- /dev/null +++ b/desktop/angular/src/app/shared/feature-scout/feature-scout.ts @@ -0,0 +1,98 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, OnInit, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { BoolSetting, ConfigService, FeatureID, Netquery, SPNService, SPNStatus, UserProfile } from "@safing/portmaster-api"; +import { catchError, of } from "rxjs"; +import { fadeInAnimation, fadeOutAnimation } from "../animations"; +import { CountryFlagModule } from 'src/app/shared/country-flag'; + +@Component({ + selector: 'app-feature-scout', + templateUrl: './feature-scout.html', + styleUrls: [ + './feature-scout.scss' + ], + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + fadeInAnimation, + fadeOutAnimation, + ] +}) +export class FeatureScoutComponent implements OnInit { + private destroyRef = inject(DestroyRef); + + /** The current SPN user profile */ + profile: UserProfile | null = null; + + /** Whether or not the SPN is currently enabled */ + spnEnabled = false; + + /** The current status of the SPN module */ + spnStatus: SPNStatus | null = null; + + /** Whether or not the Network History is currently enabled */ + historyEnabled = false; + + /** Returns whether or not the current package has the SPN feature */ + get packageHasSPN() { + return this.profile?.current_plan?.feature_ids?.includes(FeatureID.SPN) + } + + /** Returns whether or not the current package has the Network History feature */ + get packageHasHistory() { + return this.profile?.current_plan?.feature_ids?.includes(FeatureID.History) + } + + constructor( + private configService: ConfigService, + private spnService: SPNService, + private cdr: ChangeDetectorRef, + ) { } + + ngOnInit(): void { + this.spnService + .profile$ + .pipe( + takeUntilDestroyed(this.destroyRef), + catchError(() => of(null)) + ) + .subscribe(profile => { + this.profile = profile || null; + + this.cdr.markForCheck(); + }); + + this.spnService.status$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(status => { + this.spnStatus = status; + + this.cdr.markForCheck(); + }) + + this.configService.watch("spn/enable") + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => { + this.spnEnabled = value; + + this.cdr.markForCheck(); + }); + + this.configService.watch("history/enable") + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => { + this.historyEnabled = value; + + this.cdr.markForCheck(); + }); + } + + setSPNEnabled(v: boolean) { + this.configService.save(`spn/enable`, v) + .subscribe(); + } + + setHistoryEnabled(v: boolean) { + this.configService.save(`history/enable`, v) + .subscribe(); + } +} diff --git a/desktop/angular/src/app/shared/feature-scout/index.ts b/desktop/angular/src/app/shared/feature-scout/index.ts new file mode 100644 index 00000000..6fc7f610 --- /dev/null +++ b/desktop/angular/src/app/shared/feature-scout/index.ts @@ -0,0 +1 @@ +export * from './feature-scout'; diff --git a/desktop/angular/src/app/shared/focus/focus.directive.ts b/desktop/angular/src/app/shared/focus/focus.directive.ts new file mode 100644 index 00000000..79b83f40 --- /dev/null +++ b/desktop/angular/src/app/shared/focus/focus.directive.ts @@ -0,0 +1,32 @@ +import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { Directive, ElementRef, Input, OnInit } from "@angular/core"; + +@Directive({ + // eslint-disable-next-line @angular-eslint/directive-selector + selector: '[autoFocus]', +}) +export class AutoFocusDirective implements OnInit { + private _focus = true; + private _afterInit = false; + + @Input('autoFocus') + set focus(v: any) { + this._focus = coerceBooleanProperty(v) !== false; + + if (this._afterInit && this.elementRef) { + this.elementRef.nativeElement.focus() + } + } + + constructor(private elementRef: ElementRef) { } + + ngOnInit(): void { + setTimeout(() => { + if (this._focus) { + this.elementRef.nativeElement.focus(); + } + }, 100) + + this._afterInit = true; + } +} diff --git a/desktop/angular/src/app/shared/focus/focus.module.ts b/desktop/angular/src/app/shared/focus/focus.module.ts new file mode 100644 index 00000000..29593994 --- /dev/null +++ b/desktop/angular/src/app/shared/focus/focus.module.ts @@ -0,0 +1,16 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { AutoFocusDirective } from "./focus.directive"; + +@NgModule({ + imports: [ + CommonModule, + ], + declarations: [ + AutoFocusDirective, + ], + exports: [ + AutoFocusDirective, + ] +}) +export class SfngFocusModule { } diff --git a/desktop/angular/src/app/shared/focus/index.ts b/desktop/angular/src/app/shared/focus/index.ts new file mode 100644 index 00000000..f7f9cff0 --- /dev/null +++ b/desktop/angular/src/app/shared/focus/index.ts @@ -0,0 +1,2 @@ +export { AutoFocusDirective } from './focus.directive'; +export * from './focus.module'; diff --git a/desktop/angular/src/app/shared/fuzzySearch/fuse.service.ts b/desktop/angular/src/app/shared/fuzzySearch/fuse.service.ts new file mode 100644 index 00000000..d7f3e192 --- /dev/null +++ b/desktop/angular/src/app/shared/fuzzySearch/fuse.service.ts @@ -0,0 +1,105 @@ +import { Injectable } from '@angular/core'; +import { deepClone } from '@safing/portmaster-api'; +import Fuse from 'fuse.js'; + +export type FuseResult = Fuse.FuseResult; + +export interface FuseSearchOpts extends Fuse.IFuseOptions { + minSearchTermLength?: number; + maximumScore?: number; +} + +@Injectable({ + providedIn: 'root' +}) +export class FuzzySearchService { + + readonly defaultOptions: FuseSearchOpts = { + minMatchCharLength: 2, + includeMatches: true, + includeScore: true, + minSearchTermLength: 3, + }; + + searchList(list: Array, searchTerms: string, options: FuseSearchOpts & { disableHighlight?: boolean } = {}): Array> { + const opts: FuseSearchOpts = { + ...this.defaultOptions, + ...options, + } + + let result: FuseResult[] = []; + + + if (searchTerms && searchTerms.length >= (opts.minSearchTermLength || 0)) { + let fuse = new Fuse(list, opts); + result = fuse.search(searchTerms); + + } else { + result = list.map((item, index) => ({ + item: item, + refIndex: index, + score: 0, + })) + } + + if (!!options.disableHighlight) { + return result; + } + + return this.handleHighlight(result, options); + } + + private handleHighlight(result: FuseResult[], options: FuseSearchOpts): FuseResult[] { + return result.map(matchObject => { + matchObject.item = deepClone(matchObject.item); + + if (!matchObject.matches) { + return matchObject; + } + + for (let match of matchObject.matches!) { + const indices = match.indices; + + let highlightOffset: number = 0; + + for (let indice of indices) { + let initialValue = getFromMatch(matchObject, match); + + const startOffset = indice[0] + highlightOffset; + const endOffset = indice[1] + highlightOffset + 1; + + if (endOffset - startOffset < 4) { + continue + } + + let highlightedTerm = initialValue.substring(startOffset, endOffset); + let newValue = initialValue.substring(0, startOffset) + '' + highlightedTerm + '' + initialValue.substring(endOffset); + + highlightOffset += ''.length; + + setOnMatch(matchObject, match, newValue); + } + } + + return matchObject; + }); + } +} + +function getFromMatch(result: Fuse.FuseResult, match: Fuse.FuseResultMatch): string { + if (match.refIndex === undefined) { + return (result.item as any)[match.key!]; + } + return (result.item as any)[match.key!][match.refIndex]; +} + +function setOnMatch(result: Fuse.FuseResult, match: Fuse.FuseResultMatch, value: string) { + if (match.refIndex === undefined) { + (result.item as any)[match.key!] = value; + return; + } + + (result.item as any)[match.key!][match.refIndex] = value; +} diff --git a/desktop/angular/src/app/shared/fuzzySearch/index.ts b/desktop/angular/src/app/shared/fuzzySearch/index.ts new file mode 100644 index 00000000..d1194321 --- /dev/null +++ b/desktop/angular/src/app/shared/fuzzySearch/index.ts @@ -0,0 +1,4 @@ +import Fuse from 'fuse.js'; + +export { FuseSearchOpts, FuzzySearchService } from './fuse.service'; +export { FuzzySearchPipe } from './search-pipe'; diff --git a/desktop/angular/src/app/shared/fuzzySearch/search-pipe.ts b/desktop/angular/src/app/shared/fuzzySearch/search-pipe.ts new file mode 100644 index 00000000..4f6d7b1b --- /dev/null +++ b/desktop/angular/src/app/shared/fuzzySearch/search-pipe.ts @@ -0,0 +1,19 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { FuseResult, FuseSearchOpts, FuzzySearchService } from './fuse.service'; + + +@Pipe({ + name: 'fuzzySearch', +}) +export class FuzzySearchPipe implements PipeTransform { + constructor( + private FusejsService: FuzzySearchService + ) { } + + transform(elements: Array, + searchTerms: string, + options: FuseSearchOpts = {}): Array> { + + return this.FusejsService.searchList(elements, searchTerms, options); + } +} diff --git a/desktop/angular/src/app/shared/loading/index.ts b/desktop/angular/src/app/shared/loading/index.ts new file mode 100644 index 00000000..68c5f495 --- /dev/null +++ b/desktop/angular/src/app/shared/loading/index.ts @@ -0,0 +1 @@ +export { LoadingComponent } from './loading'; diff --git a/desktop/angular/src/app/shared/loading/loading.html b/desktop/angular/src/app/shared/loading/loading.html new file mode 100644 index 00000000..bfa7d9b6 --- /dev/null +++ b/desktop/angular/src/app/shared/loading/loading.html @@ -0,0 +1,3 @@ + + + diff --git a/desktop/angular/src/app/shared/loading/loading.scss b/desktop/angular/src/app/shared/loading/loading.scss new file mode 100644 index 00000000..fccdce9d --- /dev/null +++ b/desktop/angular/src/app/shared/loading/loading.scss @@ -0,0 +1,52 @@ +:host { + --internal-dot-size : var(--dot-size, 5px); + --internal-animation-speed: var(--animation-speed, 1.3s); + + display : flex; + position : relative; + justify-content: space-evenly; + align-items : flex-end; + width : var(--animation-width, calc(var(--internal-dot-size) * 5)); + + height: calc(var(--internal-dot-size) * 3); + + &.animate { + .dot { + display : block; + flex-shrink: 0; + flex-grow : 0; + width : var(--internal-dot-size); + height : var(--internal-dot-size); + + @apply shadow-inner-xs; + @apply rounded-full; + @apply bg-buttons-icon; + + animation: wave var(--internal-animation-speed) linear infinite; + + &:nth-child(2) { + animation-delay: -1.1s; + } + + &:nth-child(3) { + animation-delay: -0.9s; + } + } + } + +} + +@keyframes wave { + + 0%, + 60%, + 100% { + transform: initial; + @apply bg-buttons-light; + } + + 90% { + transform : translateY(var(--loading-height, -9px)); + background-color: white; + } +} diff --git a/desktop/angular/src/app/shared/loading/loading.ts b/desktop/angular/src/app/shared/loading/loading.ts new file mode 100644 index 00000000..fb7f049d --- /dev/null +++ b/desktop/angular/src/app/shared/loading/loading.ts @@ -0,0 +1,14 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding } from '@angular/core'; + +@Component({ + selector: 'app-loading', + templateUrl: './loading.html', + styleUrls: ['./loading.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LoadingComponent { + @HostBinding('class.animate') + _animate = true; + + constructor(private changeDetectorRef: ChangeDetectorRef) { } +} diff --git a/desktop/angular/src/app/shared/menu/index.ts b/desktop/angular/src/app/shared/menu/index.ts new file mode 100644 index 00000000..bb5dcd95 --- /dev/null +++ b/desktop/angular/src/app/shared/menu/index.ts @@ -0,0 +1,2 @@ +export { MenuComponent, MenuTriggerComponent, MenuItemComponent, MenuGroupComponent } from './menu'; +export * from './menu.module'; diff --git a/desktop/angular/src/app/shared/menu/menu-group.scss b/desktop/angular/src/app/shared/menu/menu-group.scss new file mode 100644 index 00000000..c2cd7063 --- /dev/null +++ b/desktop/angular/src/app/shared/menu/menu-group.scss @@ -0,0 +1,13 @@ +:host { + display: block; + width: 100%; + + @apply p-1; + @apply px-4; + @apply text-secondary; + + display: block; + text-transform: uppercase; + font-size: 0.7rem; + opacity: .7; +} diff --git a/desktop/angular/src/app/shared/menu/menu-item.scss b/desktop/angular/src/app/shared/menu/menu-item.scss new file mode 100644 index 00000000..fdba9c32 --- /dev/null +++ b/desktop/angular/src/app/shared/menu/menu-item.scss @@ -0,0 +1,17 @@ +:host { + @apply block w-full; + + cursor: pointer; + @apply p-2; + @apply px-4 text-primary text-xxs; + font-weight: 500; + + &:hover { + @apply bg-gray-300; + } + + &.disabled { + cursor: not-allowed; + opacity: 0.5; + } +} diff --git a/desktop/angular/src/app/shared/menu/menu-trigger.html b/desktop/angular/src/app/shared/menu/menu-trigger.html new file mode 100644 index 00000000..02642109 --- /dev/null +++ b/desktop/angular/src/app/shared/menu/menu-trigger.html @@ -0,0 +1,14 @@ +
+ +
diff --git a/desktop/angular/src/app/shared/menu/menu-trigger.scss b/desktop/angular/src/app/shared/menu/menu-trigger.scss new file mode 100644 index 00000000..77cc16b0 --- /dev/null +++ b/desktop/angular/src/app/shared/menu/menu-trigger.scss @@ -0,0 +1,41 @@ +:host { + user-select: none; + margin-right: .5rem; + display: block; + @apply rounded-t-sm; +} + +div { + cursor: pointer; + display: flex; + @apply rounded-t; + flex-grow: 0; + transition: all .1s ease-in-out; + justify-content: center; + align-items: center; + @apply py-1; + @apply px-3; +} + +.dropdown { + margin-left: 1px; + height: auto; + padding: 0; + margin: 0; + + svg { + opacity: 0.7; + fill: var(--text-primary); + width: 0.51rem; + transition: all cubic-bezier(0.175, 0.885, 0.32, 1.275) .2s; + + transform: rotate(90deg); + position: relative; + top: 3px; + } +} + +:host.active { + @apply bg-gray-400; + color: white !important; +} diff --git a/desktop/angular/src/app/shared/menu/menu.html b/desktop/angular/src/app/shared/menu/menu.html new file mode 100644 index 00000000..d33da3d7 --- /dev/null +++ b/desktop/angular/src/app/shared/menu/menu.html @@ -0,0 +1,6 @@ + +
+ +
+
diff --git a/desktop/angular/src/app/shared/menu/menu.module.ts b/desktop/angular/src/app/shared/menu/menu.module.ts new file mode 100644 index 00000000..4f97a6c0 --- /dev/null +++ b/desktop/angular/src/app/shared/menu/menu.module.ts @@ -0,0 +1,26 @@ +import { OverlayModule } from "@angular/cdk/overlay"; +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { SfngDropDownModule } from "@safing/ui"; +import { MenuComponent, MenuGroupComponent, MenuItemComponent, MenuTriggerComponent } from "./menu"; + +@NgModule({ + imports: [ + SfngDropDownModule, + CommonModule, + OverlayModule, + ], + declarations: [ + MenuComponent, + MenuGroupComponent, + MenuTriggerComponent, + MenuItemComponent, + ], + exports: [ + MenuComponent, + MenuGroupComponent, + MenuTriggerComponent, + MenuItemComponent, + ], +}) +export class SfngMenuModule { } diff --git a/desktop/angular/src/app/shared/menu/menu.ts b/desktop/angular/src/app/shared/menu/menu.ts new file mode 100644 index 00000000..f5467921 --- /dev/null +++ b/desktop/angular/src/app/shared/menu/menu.ts @@ -0,0 +1,111 @@ +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { CdkOverlayOrigin } from '@angular/cdk/overlay'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, EventEmitter, HostBinding, HostListener, Input, Output, QueryList, ViewChild } from '@angular/core'; +import { SfngDropdownComponent } from '@safing/ui'; + +@Component({ + selector: 'app-menu-trigger', + templateUrl: './menu-trigger.html', + styleUrls: ['./menu-trigger.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MenuTriggerComponent { + @ViewChild(CdkOverlayOrigin, { static: true }) + origin!: CdkOverlayOrigin; + + @Input() + menu: MenuComponent | null = null; + + @Input() + set useContent(v: any) { + this._useContent = coerceBooleanProperty(v); + } + get useContent() { return this._useContent; } + private _useContent: boolean = false; + + @HostBinding('class.active') + get isOpen() { + if (!this.menu) { + return false; + } + + return this.menu.dropdown.isOpen; + } + + constructor( + public changeDetectorRef: ChangeDetectorRef, + ) { } + + toggle(event: MouseEvent) { + event.preventDefault(); + event.stopPropagation(); + + this.menu?.dropdown.toggle(this.origin) + } +} + +@Component({ + selector: 'app-menu-item', + template: '', + styleUrls: ['./menu-item.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MenuItemComponent { + @Input() + @HostBinding('class.disabled') + set disabled(v: any) { + this._disabled = coerceBooleanProperty(v); + } + get disabled() { return this._disabled; } + private _disabled: boolean = false; + + @HostListener('click', ['$event']) + closeMenu(event: MouseEvent) { + if (this.disabled) { + return; + } + this.activate.next(event); + this.menu.dropdown.close(); + } + + /** + * activate fires when the menu item is clicked. + * Use activate rather than (click)="" if you want + * [disabled] to be considered. + */ + @Output() + activate = new EventEmitter(); + + constructor(private menu: MenuComponent) { } +} + +@Component({ + selector: 'app-menu-group', + template: '', + styleUrls: ['./menu-group.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MenuGroupComponent { } + +@Component({ + selector: 'app-menu', + exportAs: 'appMenu', + templateUrl: './menu.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MenuComponent { + @ContentChildren(MenuItemComponent) + items: QueryList | null = null; + + @ViewChild(SfngDropdownComponent, { static: true }) + dropdown!: SfngDropdownComponent; + + @Input() + offsetY?: string | number; + + @Input() + offsetX?: string | number; + + @Input() + overlayClass?: string; +} diff --git a/desktop/angular/src/app/shared/multi-switch/index.ts b/desktop/angular/src/app/shared/multi-switch/index.ts new file mode 100644 index 00000000..6009e482 --- /dev/null +++ b/desktop/angular/src/app/shared/multi-switch/index.ts @@ -0,0 +1,3 @@ +export { MultiSwitchComponent } from './multi-switch'; +export { SwitchItemComponent } from './switch-item'; +export * from './multi-switch.module'; diff --git a/desktop/angular/src/app/shared/multi-switch/multi-switch.html b/desktop/angular/src/app/shared/multi-switch/multi-switch.html new file mode 100644 index 00000000..6af6d004 --- /dev/null +++ b/desktop/angular/src/app/shared/multi-switch/multi-switch.html @@ -0,0 +1,5 @@ +
+ + +
+ diff --git a/desktop/angular/src/app/shared/multi-switch/multi-switch.module.ts b/desktop/angular/src/app/shared/multi-switch/multi-switch.module.ts new file mode 100644 index 00000000..a3404d52 --- /dev/null +++ b/desktop/angular/src/app/shared/multi-switch/multi-switch.module.ts @@ -0,0 +1,26 @@ +import { DragDropModule } from "@angular/cdk/drag-drop"; +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { SfngTipUpModule, SfngTooltipModule } from "@safing/ui"; +import { MultiSwitchComponent } from "./multi-switch"; +import { SwitchItemComponent } from "./switch-item"; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + SfngTooltipModule, + SfngTipUpModule, + DragDropModule, + ], + declarations: [ + MultiSwitchComponent, + SwitchItemComponent, + ], + exports: [ + MultiSwitchComponent, + SwitchItemComponent, + ], +}) +export class SfngMultiSwitchModule { } diff --git a/desktop/angular/src/app/shared/multi-switch/multi-switch.scss b/desktop/angular/src/app/shared/multi-switch/multi-switch.scss new file mode 100644 index 00000000..6b96e2f3 --- /dev/null +++ b/desktop/angular/src/app/shared/multi-switch/multi-switch.scss @@ -0,0 +1,46 @@ +.buttons { + display: flex; + align-items: flex-end; + position: relative; + height: 3rem; + flex-grow: 0; + width: fit-content; + + fa-icon[icon*="question-circle"] { + height: 100%; + display: flex; + align-items: center; + margin-left: 1rem; + } +} + +.marker { + display: block; + height: 16px; + width: 16px; + position: absolute; + bottom: -8px; + cursor: grab; + transition: all .5s cubic-bezier(0.175, 0.885, 0.32, 1.075); + @apply rounded-full; +} + +:host { + flex-grow: 0; + width: fit-content; + display: block; + outline: none; + user-select: none; + + &.disabled { + .marker { + cursor: unset; + } + } + + &.grabbing { + .marker { + cursor: grabbing; + } + } +} diff --git a/desktop/angular/src/app/shared/multi-switch/multi-switch.ts b/desktop/angular/src/app/shared/multi-switch/multi-switch.ts new file mode 100644 index 00000000..2726427c --- /dev/null +++ b/desktop/angular/src/app/shared/multi-switch/multi-switch.ts @@ -0,0 +1,370 @@ +import { ListKeyManager } from '@angular/cdk/a11y'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { DOCUMENT } from '@angular/common'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, ElementRef, EventEmitter, forwardRef, HostBinding, HostListener, Inject, Input, NgZone, OnDestroy, Output, QueryList, Renderer2, ViewChild } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { animationFrameScheduler, fromEvent, Subscription } from 'rxjs'; +import { map, startWith, subscribeOn, take, takeUntil } from 'rxjs/operators'; +import { SwitchItemComponent } from './switch-item'; + +@Component({ + selector: 'app-multi-switch', + templateUrl: './multi-switch.html', + styleUrls: ['./multi-switch.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MultiSwitchComponent), + multi: true, + } + ] +}) +export class MultiSwitchComponent implements OnDestroy, AfterViewInit, ControlValueAccessor { + /** Subscription to all button-select changes */ + private sub = Subscription.EMPTY; + + /** Holds the current x-translation offset for the marker */ + private markerOffset: number = 0; + + /** Keymanager used for keyboard navigation support */ + private keyManager: ListKeyManager> | null = null; + + /** Subscription to the key manager */ + private keyManagerSub = Subscription.EMPTY; + + @Input() + tipUpKey: string = ''; + + /** All buttons projected into the multi-switch */ + @ContentChildren(SwitchItemComponent) + buttons: QueryList> | null = null; + + /** Emits whenever the selected button changes. */ + @Output() + changed = new EventEmitter(); + + /** Reference to the marker inside our view container */ + @ViewChild('marker', { read: ElementRef, static: true }) + marker: ElementRef | null = null; + + @HostListener('blur') + onBlur() { + this._onTouch(); + } + + @HostBinding('attr.tabindex') + readonly tabindex = 0; + + @HostListener('keyup', ['$event']) + onKeyUp(event: KeyboardEvent) { + if (this.disabled) { + return; + } + this.keyManager!.onKeydown(event); + } + + /** Whether or not the switch button component is disabled */ + @Input() + @HostBinding('class.disabled') + set disabled(v: any) { + this._disabled = coerceBooleanProperty(v); + + // Update all buttons states as well. + if (!!this.buttons) { + this.buttons.forEach(btn => btn.disabled = this.disabled); + } + } + get disabled() { return this._disabled; } + private _disabled = false; + + @HostBinding('class.grabbing') + isGrabbing = false; + + /** External write tracks calls to writeValue so we don't end up re-emitting the values. */ + private externalWrite = false; + + /** Which button is currently active (and holds the marker) */ + activeButton: T | null = null; + + constructor( + public host: ElementRef, + private changeDetectorRef: ChangeDetectorRef, + private renderer: Renderer2, + private ngZone: NgZone, + @Inject(DOCUMENT) private document: Document, + ) { } + + /** Registeres the change callback. Required for ControlValueAccessor */ + registerOnChange(fn: (v: T) => void) { + this._onChange = fn; + } + private _onChange: (value: T) => void = () => { } + + /** Registers the touch callback. Required for ControlValueAccessor */ + registerOnTouched(fn: () => void) { + this._onTouch = fn; + } + private _onTouch: () => void = () => { }; + + /** Disable or enable the button. Required for ControlValueAccessor */ + setDisabledState(disabled: boolean) { + this.disabled = disabled; + } + + /** Writes a new value for the multi-line switch */ + writeValue(value: T) { + this.activeButton = value; + if (!!this.buttons) { + // Set externalWrite to true while we iterate the buttons + // and eventually call `setActiveItem` so we don't re-emit + // the active item once the keyManager publishes the change + // to use. + // This workaround is required as we need to inform the + // keyManager about the new active item. Otherwise it would + // work with a stale internal state the next time the user + // uses the keyboard. + this.externalWrite = true; + this.buttons.forEach(btn => { + if (btn.id === value) { + this.keyManager!.setActiveItem(btn); + this.repositionMarker(btn); + } + }) + this.externalWrite = false; + } + } + + ngAfterViewInit() { + if (!this.buttons) { + return; + } + + this.keyManager = new ListKeyManager(this.buttons) + .withHorizontalOrientation('ltr') + .withTypeAhead() + .withWrap(); + + this.keyManagerSub = this.keyManager.change + .subscribe(activeIndex => { + const active = Array.from(this.buttons!)[activeIndex]; + this.selectButton(active, !this.externalWrite); + }); + + // Subscribe to all (clicked) and (selectedChange) events of + // all buttons projected into our content. + this.buttons.changes + .pipe(startWith(null)) + .subscribe(() => { + this.sub.unsubscribe(); + this.sub = new Subscription(); + + this.buttons!.forEach(btn => { + btn.disabled = this.disabled; + this.sub.add( + btn.clicked.subscribe((e: MouseEvent) => { + this.keyManager!.setActiveItem(btn); + }) + ); + }); + + // wait until the zone and change-detection stabilizes and + // reposition the marker afterwards. Doing it right now will + // likely position it wrongly since the DOM has not yet been + // fully updated. + this.ngZone.onStable.pipe(take(1)) + .subscribe(() => this.repositionMarker()) + }); + + this.buttons.forEach(btn => { + if (this.activeButton === btn.id) { + btn.selected = true; + } + }) + + this.repositionMarker(); + } + + ngOnDestroy() { + this.sub.unsubscribe(); + this.keyManagerSub.unsubscribe(); + } + + /** Selects a new button and deselects all others. */ + private selectButton(btn: SwitchItemComponent, emit = true) { + if (this.disabled) { + return; + } + + this.activeButton = btn.id; + + if (emit) { + this.changed.next(btn.id!); + this._onChange(btn.id!); + } + + this.repositionMarker(btn); + } + + /** @private View-callback for (mousedown) to start dragging the marker. */ + dragStarted(event: MouseEvent) { + if (this.disabled) { + return; + } + + this.isGrabbing = true; + this.renderer.addClass(this.document.getElementsByTagName("body")[0], 'document-grabbing'); + + const mousemove$ = fromEvent(this.document, 'mousemove'); + const hostRect = this.host.nativeElement.getBoundingClientRect(); + const start = this.markerOffset; + const markerWidth = this.marker!.nativeElement.getBoundingClientRect().width; + + // we don't want angular to run change detection all the time we move a pixel + // so detach the change-detector for now. + this.changeDetectorRef.detach(); + + mousemove$ + .pipe( + map(move => { + move.preventDefault(); + return move.clientX - event.clientX; + }), + takeUntil(fromEvent(document, 'mouseup')), + subscribeOn(animationFrameScheduler) + ) + .subscribe({ + next: diff => { + // clip the new offset inside our host-view. + let offset = start + diff; + if (offset < 0) { + offset = 0; + } else if (offset > hostRect.width) { + offset = hostRect.width; + } + + // center the marker at the mouse position. + offset -= Math.round(markerWidth / 2); + + this.markerOffset = offset; + this.updatePosition(offset); + + let foundTarget = false; + let target = this.findTargetButton(offset); + + if (!!target) { + this.marker!.nativeElement.style.backgroundColor = target.borderColorActive; + + this.buttons!.forEach(btn => { + if (!foundTarget && btn.group === target!.group) { + this.renderer.addClass(btn.elementRef.nativeElement, 'selected'); + btn.elementRef.nativeElement.style.borderColor = btn.borderColorActive; + } else { + this.renderer.removeClass(btn.elementRef.nativeElement, 'selected'); + btn.elementRef.nativeElement.style.borderColor = btn.borderColorInactive; + } + + if (target === btn) { + foundTarget = true; + } + }); + } + }, + complete: () => { + this.changeDetectorRef.reattach(); + this.markerDropped(); + + // make sure we don't keep the selected class on buttons that + // are not selected anymore. + this.buttons!.forEach(btn => { + if (!btn.selected) { + this.renderer.removeClass(btn.elementRef.nativeElement, 'selected'); + btn.elementRef.nativeElement.style.borderColor = btn.borderColorInactive; + } + }); + + this.isGrabbing = false; + this.renderer.removeClass(this.document.getElementsByTagName("body")[0], 'document-grabbing'); + } + }); + } + + /** Update the markers position by applying a translate3d */ + private updatePosition(x: number) { + this.marker!.nativeElement.style.transform = `translate3d(${x}px, 0px, 0px)`; + } + + /** Find the button item that is below x */ + private findTargetButton(x: number, cb?: (item: SwitchItemComponent, target: boolean) => void): SwitchItemComponent | null { + const host = this.host.nativeElement.getBoundingClientRect(); + let newButton: SwitchItemComponent | null = null; + this.buttons?.forEach(btn => { + const btnRect = btn.elementRef.nativeElement.getBoundingClientRect(); + const min = btnRect.x - host.x; + const max = min + btnRect.width; + + if (x >= min && x <= max) { + newButton = btn; + + if (!!cb) { + cb(btn, true); + } + } else if (!!cb) { + cb(btn, false); + } + }); + + return newButton; + } + + /** Calculates which button should be activated based on the drop-position of the marker */ + private markerDropped() { + let newButton = this.findTargetButton(this.markerOffset); + + if (!newButton) { + newButton = Array.from(this.buttons!)[0]; + } + + if (!!newButton) { + this.keyManager!.setActiveItem(newButton); + } + } + + /** + * Calculates the new position required to center the + * marker at the currently selected button. + * If `selected` is unset the last button with selected == true is + * used. + * + * @param selected The switch item button to select (optional). + */ + private repositionMarker(selected: SwitchItemComponent | null = null) { + // If there's no selected button given search for the last one that + // matches selected === true. + if (selected === null) { + this.buttons?.forEach(btn => { + if (btn.selected) { + selected = btn; + } + }); + } + + // There's not button selected so we move the marker back to the + // start. + if (selected === null) { + this.markerOffset = 0; + this.updatePosition(0); + return; + } + + // Calculate and reposition the marker. + const offsetLeft = selected!.elementRef.nativeElement.offsetLeft; + const clientWidth = selected!.elementRef.nativeElement.clientWidth; + + this.markerOffset = Math.round(offsetLeft - 8 + clientWidth / 2); + this.marker!.nativeElement.style.backgroundColor = selected.borderColorActive; + + this.updatePosition(this.markerOffset); + this.changeDetectorRef.markForCheck(); + } +} diff --git a/desktop/angular/src/app/shared/multi-switch/switch-item.scss b/desktop/angular/src/app/shared/multi-switch/switch-item.scss new file mode 100644 index 00000000..da737c13 --- /dev/null +++ b/desktop/angular/src/app/shared/multi-switch/switch-item.scss @@ -0,0 +1,35 @@ +:host { + display : flex; + align-items : center; + justify-content: center; + width : 6rem; + height : 2.7rem; + position : relative; + bottom : 0; + transition : all .3s cubic-bezier(0.075, 0.82, 0.165, 1); + + @apply bg-buttons-dark; + + @apply border-b-2; + + &.selected { + @apply bg-buttons-light; + height: 3rem; + } + + &:not(.disabled) { + cursor: pointer; + + &:hover { + @apply bg-buttons-light; + } + } + + &:first-of-type { + @apply rounded-tl; + } + + &:last-of-type { + @apply rounded-tr; + } +} diff --git a/desktop/angular/src/app/shared/multi-switch/switch-item.ts b/desktop/angular/src/app/shared/multi-switch/switch-item.ts new file mode 100644 index 00000000..f70f3a2f --- /dev/null +++ b/desktop/angular/src/app/shared/multi-switch/switch-item.ts @@ -0,0 +1,80 @@ +import { Component, ChangeDetectionStrategy, Input, isDevMode, OnInit, HostBinding, Output, EventEmitter, HostListener, ElementRef, ChangeDetectorRef } from '@angular/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +@Component({ + selector: 'app-switch-item', + template: '', + styleUrls: ['./switch-item.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SwitchItemComponent implements OnInit { + @Input() + id: T | null = null; + + @Input() + group = ''; + + @Output() + clicked = new EventEmitter(); + + @HostListener('click', ['$event']) + onClick(e: MouseEvent) { + this.clicked.next(e); + } + + @Input() + borderColorActive: string = 'var(--info-green)'; + + @Input() + borderColorInactive: string = 'var(--button-light)'; + + @HostBinding('style.border-color') + get borderColor() { + if (this.selected) { + return this.borderColorActive; + } + return this.borderColorInactive; + } + + @Input() + @HostBinding('class.disabled') + set disabled(v: any) { + this._disabled = coerceBooleanProperty(v); + } + get disabled() { + return this._disabled; + } + private _disabled = false; + + @Input() + @HostBinding('class.selected') + set selected(v: any) { + const selected = coerceBooleanProperty(v); + if (selected !== this._selected) { + this._selected = selected; + this.selectedChange.next(selected); + } + } + get selected() { + return this._selected; + } + private _selected = false; + + getLabel() { + return this.elementRef.nativeElement.innerText; + } + + @Output() + selectedChange = new EventEmitter(); + + ngOnInit() { + if (this.id === null && isDevMode()) { + throw new Error(`SwitchItemComponent must have an ID`); + } + } + + constructor( + public readonly elementRef: ElementRef, + public readonly changeDetectorRef: ChangeDetectorRef, + ) { } +} diff --git a/desktop/angular/src/app/shared/netquery/.eslintrc.json b/desktop/angular/src/app/shared/netquery/.eslintrc.json new file mode 100644 index 00000000..5ac41541 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/.eslintrc.json @@ -0,0 +1,44 @@ +{ + "extends": "../../../../.eslintrc.json", + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts" + ], + "parserOptions": { + "project": [ + "projects/safing/ui/tsconfig.lib.json", + "projects/safing/ui/tsconfig.spec.json" + ], + "createDefaultProgram": true + }, + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "sfng", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "sfng", + "style": "kebab-case" + } + ] + } + }, + { + "files": [ + "*.html" + ], + "rules": {} + } + ] +} diff --git a/desktop/angular/src/app/shared/netquery/add-to-filter/add-to-filter.ts b/desktop/angular/src/app/shared/netquery/add-to-filter/add-to-filter.ts new file mode 100644 index 00000000..f87d4910 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/add-to-filter/add-to-filter.ts @@ -0,0 +1,93 @@ +import { ChangeDetectorRef, Directive, HostBinding, HostListener, Input, OnDestroy, OnInit, inject } from "@angular/core"; +import { NetqueryConnection } from "@safing/portmaster-api"; +import { Subscription, combineLatest } from "rxjs"; +import { ActionIndicatorService } from "../../action-indicator"; +import { NetqueryHelper } from "../connection-helper.service"; +import { INTEGRATION_SERVICE } from "src/app/integration"; + +@Directive({ + selector: '[sfngAddToFilter]' +}) +export class SfngNetqueryAddToFilterDirective implements OnInit, OnDestroy { + private subscription = Subscription.EMPTY; + private readonly integration = inject(INTEGRATION_SERVICE); + + @Input('sfngAddToFilter') + key: keyof NetqueryConnection | null = null; + + @Input('sfngAddToFilterValue') + set value(v: any | any[]) { + if (!Array.isArray(v)) { + v = [v] + } + this._values = v; + } + private _values: any[] = []; + + @HostListener('click', ['$event']) + onClick(evt: MouseEvent) { + if (!this.key) { + return + } + + let prevent = false + if (evt.shiftKey) { + this.helper.addToFilter(this.key, this._values); + prevent = true + } else if (evt.ctrlKey) { + this.integration.writeToClipboard(this._values.join(', ')) + .then(() => { + this.uai.success("Copied to clipboard", "Successfully copied " + this._values.join(", ") + " to your clipboard") + }) + .catch(err => { + this.uai.error("Failed to copy to clipboard", this.uai.getErrorMessgae(err)) + }) + + prevent = true + } + + if (prevent) { + evt.preventDefault(); + evt.stopPropagation(); + } + } + + @HostBinding('class.border-dashed') + @HostBinding('class.border-gray-500') + @HostBinding('class.hover:border-gray-700') + readonly _styleHost = true; + + @HostBinding('class.cursor-pointer') + @HostBinding('class.hover:cursor-pointer') + @HostBinding('class.border-b') + @HostBinding('class.select-none') + get shouldHiglight() { + return this.isShiftKeyPressed || this.isCtrlKeyPressed + } + + isShiftKeyPressed = false; + isCtrlKeyPressed = false; + + constructor( + private helper: NetqueryHelper, + private uai: ActionIndicatorService, + private cdr: ChangeDetectorRef, + ) { } + + ngOnInit(): void { + this.subscription = combineLatest([this.helper.onShiftKey, this.helper.onCtrlKey]) + .subscribe(([isShiftKeyPressed, isCtrlKeyPressed]) => { + if (!this.key) { + return; + } + + this.isShiftKeyPressed = isShiftKeyPressed; + this.isCtrlKeyPressed = isCtrlKeyPressed; + this.cdr.markForCheck(); + }) + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } +} diff --git a/desktop/angular/src/app/shared/netquery/add-to-filter/index.ts b/desktop/angular/src/app/shared/netquery/add-to-filter/index.ts new file mode 100644 index 00000000..1dfab44f --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/add-to-filter/index.ts @@ -0,0 +1 @@ +export * from './add-to-filter'; diff --git a/desktop/angular/src/app/shared/netquery/circular-bar-chart/circular-bar-chart.component.ts b/desktop/angular/src/app/shared/netquery/circular-bar-chart/circular-bar-chart.component.ts new file mode 100644 index 00000000..f2b736d9 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/circular-bar-chart/circular-bar-chart.component.ts @@ -0,0 +1,358 @@ +import { AfterViewInit, ChangeDetectionStrategy, Component, DestroyRef, ElementRef, Input, OnInit, inject } from '@angular/core'; +import { QueryResult } from '@safing/portmaster-api'; +import * as d3 from 'd3'; + +export interface CircularBarChartConfig { + // stack either holds the attribute name or an accessor function + // to determine which serieses belong to the same stack. + stack: keyof T | ((d: T) => string); + + // series either holds the attribute name of the key or an accessor function. + seriesKey: keyof T | ((d: T) => string); + + seriesLabel?: (s: string) => string; + + // value either holds the attribute name or an accessor function + // to get the value of the series. + value: keyof T | ((d: T) => number); + + colorAsClass?: boolean; + + // the actual series configuration + series?: { + [key: string]: { + color: string; + } + }; + + // The number of ticks for the y axis + ticks?: number; + + formatTick?: (v: number) => string; + + // an optional function to format the value + formatValue?: (stack: string, series: string, value: number, data?: T) => string; + + formatStack?: (sel: d3.Selection, data: T[]) => d3.Selection; +} + + +export function splitQueryResult(results: T[], series: K[]): (QueryResult & { series: string, value: number })[] { + let mapped: (QueryResult & { series: string, value: number })[] = []; + + results.forEach(row => { + series.forEach(seriesKey => { + mapped.push({ + ...row, + value: row[seriesKey], + series: seriesKey as string, + }) + }) + }) + + return mapped +} + +@Component({ + selector: 'sfng-netquery-circular-bar-chart', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CircularBarChartComponent implements OnInit, AfterViewInit { + private readonly elementRef = inject(ElementRef) as ElementRef; + private readonly destroyRef = inject(DestroyRef); + + // D3 related members + private svg?: d3.Selection; + private x?: d3.ScaleBand; + private y?: d3.ScaleRadial; + private height = 0; + private width = 0; + + @Input() + config: CircularBarChartConfig | null = null; + + @Input() + innerRadius?: number; + + @Input() + set data(d: T[] | null) { + this._data = d || []; + + this.prepareChart() + this.render(); + } + private _data: T[] = []; + + ngOnInit(): void { + this.prepareChart() + this.render() + } + + ngAfterViewInit(): void { + const observer = new ResizeObserver(() => { + this.prepareChart() + this.render() + }) + + observer.observe(this.elementRef.nativeElement) + + this.destroyRef.onDestroy(() => observer.disconnect()) + + this.prepareChart() + this.render(); + } + + private prepareChart() { + if (!!this.svg) { + const parent = this.svg.node()?.parentElement + parent?.remove() + } + + const margin = 0.2 + const bbox = this.elementRef.nativeElement.getBoundingClientRect(); + + const marginLeft = bbox.width * margin; + const marginTop = bbox.height * margin; + this.width = bbox.width - 2 * marginLeft; + this.height = bbox.height - 2 * marginTop; + + this.svg = d3.select(this.elementRef.nativeElement) + .append('svg') + .attr('width', "100%") + .attr('height', "100%") + .append('g') + .attr('transform', `translate(${this.width / 2 + marginLeft}, ${this.height / 2 + marginTop})`); + + + this.x = d3.scaleBand() + .range([0, 2 * Math.PI]) + .align(0); + + this.y = d3.scaleRadial() + + // prepare the SVGGElement that we use for rendering + this.svg.append("g") + .attr("id", "chart") + + this.svg.append("g") + .attr("id", "text") + + this.svg.append("g") + .attr("id", "legend") + + this.svg.append("g") + .attr("id", "ticks") + } + + private render() { + const x = this.x; + const y = this.y; + + if (!this.svg || !x || !y) { + console.log("not yet ready") + return; + } + + let stackName: (d: T) => string; + if (typeof this.config?.stack === 'function') { + stackName = this.config.stack; + } else { + stackName = (d: T) => { + return d[this.config!.stack as keyof T] + '' + } + } + + let seriesKey: (d: T) => string; + if (typeof this.config?.seriesKey === 'function') { + seriesKey = this.config!.seriesKey + } else { + seriesKey = (d: T) => { + return d[this.config!.seriesKey as keyof T] + '' + } + } + + let value: (d: T) => number; + if (typeof this.config?.value === 'function') { + value = this.config!.value + } else { + value = (d: T) => { + return +d[this.config!.value as keyof T] + } + } + + let formatValue: Exclude["formatValue"], undefined> = (stack, series, value) => `${stack} ${series}\n${value}` + if (this.config?.formatValue) { + formatValue = this.config.formatValue; + } + + // Prepare the stacked data + const indexed = d3.index(this._data, stackName, seriesKey) + const stackGenerator = d3.stack<[string, d3.InternMap]>() + .keys(d3.union(this._data.map(seriesKey))) + .value((data, key) => { + const obj = data[1].get(key) + if (obj === undefined) { + return 0 + } + + return value(obj); + }) + + const series = stackGenerator(indexed) + + // Prepare the x domain + const labels = new Set(); + this._data.forEach(d => labels.add(stackName(d))); + this.x!.domain(Array.from(labels)) + .range([0, 2 * Math.PI]) + .align(0); + + const innerRadius = this.innerRadius || (() => { + return (series.length * 25) + 20 + })() + + // Prepare the x domain + const outerRadius = Math.min(this.width, this.height) / 2; + const highest = d3.max(series, point => d3.max(point, point => point[1])!)! + this.y!.domain([0, highest]) + .range([innerRadius, outerRadius]); + + + const arc = d3.arc() + .innerRadius((d: any) => y(d[0])) + .outerRadius((d: any) => y(d[1])) + .startAngle((d: any) => x(d.data[0])!) + .endAngle((d: any) => x(d.data[0])! + x.bandwidth()) + .padAngle(0.01) + .padRadius(innerRadius) + + let color: (key: string) => string; + + if (!this.config?.series) { + const colorScale: d3.ScaleOrdinal = d3.scaleOrdinal() + .domain(series.map(d => d.key)) + .range(d3.schemeSpectral) + .unknown("#ccc") + + color = key => colorScale(key); + } else { + color = key => this.config!.series![key].color + } + + this.svg.select("g#chart") + .selectAll() + .data(series) + .join("g") + .call(g => { + if (this.config?.colorAsClass) { + g.attr("fill", "currentColor") + .attr("class", d => color(d.key)) + } else { + g.attr("fill", d => color(d.key)) + } + }) + .selectAll("path") + .data(D => D.map(d => ((d as any).key = D.key, d))) + .join("path") + .attr("d", arc as any) + .append("title") + .text(d => { + const stack = d.data[0] + const series = (d as any).key + const data = d.data[1].get(series); + const seriesValue = data ? value(data) : 0; + + return formatValue(stack, series, seriesValue, data); + }) + + const sumPerLabel = this._data.reduce((map, current) => { + const stack = stackName(current) + let sum = map.get(stack) || 0 + sum += value(current) + map.set(stack, sum) + + return map + }, new Map()); + + this.svg.select("g#text") + .attr("text-anchor", "middle") + .selectAll() + .data(x.domain()) + .join("g") + .attr("text-anchor", d => (x(d)! + x.bandwidth() / 2 + Math.PI) % (2 * Math.PI) < Math.PI ? "end" : "start") + .attr("transform", d => "rotate(" + ((x(d)! + this.x!.bandwidth() / 2) * 180 / Math.PI - 90) + ")" + "translate(" + (y(sumPerLabel.get(d)!) + 10) + ",0)") + .append("g") + .attr("transform", d => (x(d)! + x.bandwidth() / 2 + Math.PI) % (2 * Math.PI) < Math.PI ? "rotate(180)" : "rotate(0)") + .style("font-size", "11px") + .attr("alignment-baseline", "middle") + .attr("fill", "currentColor") + .attr("class", "text-primary cursor-pointer") + .on("mouseenter", function (data) { + d3.select(this) + .classed("underline", true) + }) + .on("mouseleave", function (data) { + d3.select(this) + .classed("underline", false) + }) + .call(g => { + if (!this.config?.formatStack) { + return g.append("text") + .text(d => `${d}`) + } + + return this.config.formatStack(g as any, this._data) + }) + + // y axis + const tickCount = this.config?.ticks || Math.floor((outerRadius - innerRadius) / 20) + const tickFormat = this.config?.formatTick || y.tickFormat(tickCount, "s") + this.svg.select("g#ticks") + .attr("text-anchor", "middle") + .selectAll("g") + .data(y.ticks(tickCount).slice(1)) + .join("g") + .attr("fill", "none") + .call(g => g.append("circle") + .attr("stroke", "#fff") + .attr("stroke-opacity", 0.25) + .attr("r", y)) + .call(g => g.append("text") + .style("font-size", "0.6rem") + .attr("y", d => -y(d)) + .attr("dy", "0.35em") + .attr("fill", "currentColor") + .attr("class", "text-secondary") + .text(tickFormat)) + + // color legend + this.svg.select("g#legend") + .selectAll() + .data(series.map(s => s.key)) + .join("g") + .attr("transform", (d, i, nodes) => `translate(-40,${(nodes.length / 2 - i - 1) * 20})`) + .call(g => g.append("circle") + .attr("r", 5) + .call(g => { + if (this.config?.colorAsClass) { + g.attr("fill", "currentColor") + .attr("class", d => color(d)) + } else { + g.attr("fill", d => color(d)) + } + })) + .call(g => g.append("text") + .attr("x", 12) + .attr("y", 4) + .attr("font-size", "0.6rem") + .attr("fill", "#fff") + .text(d => { + if (!!this.config?.seriesLabel) { + return this.config.seriesLabel(d) + } + + return d + })); + } +} diff --git a/desktop/angular/src/app/shared/netquery/combined-menu.pipe.ts b/desktop/angular/src/app/shared/netquery/combined-menu.pipe.ts new file mode 100644 index 00000000..610e15a1 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/combined-menu.pipe.ts @@ -0,0 +1,16 @@ +import { KeyValue } from '@angular/common'; +import { Pipe, PipeTransform } from "@angular/core"; + +interface Model { + visible: boolean | 'combinedMenu'; +} + +@Pipe({ + pure: true, + name: 'combinedMenu' +}) +export class CombinedMenuPipe implements PipeTransform { + transform(value: KeyValue[], ...args: any[]) { + return value.filter(entry => entry.value?.visible === 'combinedMenu') + } +} diff --git a/desktop/angular/src/app/shared/netquery/connection-details/conn-details.html b/desktop/angular/src/app/shared/netquery/connection-details/conn-details.html new file mode 100644 index 00000000..80d604f6 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/connection-details/conn-details.html @@ -0,0 +1,322 @@ +
+
+ + Started: + + {{ conn.started | date:'medium'}} + + + + + Ended: + + {{ conn.ended | date:'medium'}} + + + + + + + + Duration: + + {{ [conn.ended, conn.started] | duration }} + + + + + Profile Revision: + + {{ conn.profile_revision }} + + + + + Connection ID: + + {{ conn.id }} + + + + + Verdict: + + {{ verdict[conn.verdict] || 'N/A' }} + + + + + Internal Connection: + + {{ conn.internal ? 'Yes' : 'No' }} + + + + + Local Address: + + {{ conn.local_ip }} + {{ ':'+conn.local_port }} + + +
+ +
+ + Direction: + + + + {{ conn.direction === 'inbound' ? 'Incoming' : 'Outgoing' }} + + + + Protocol: + {{ Protocols[conn.ip_protocol] || 'N/A' }} + + + Encrypted: + {{ conn.encrypted ? 'yes' : 'no' }} + + + SPN Protected: + {{ conn.tunneled ? 'yes' : 'no' }} + + + + Data Received: + {{ conn.bytes_received | bytes }} + + + Data Sent: + {{ conn.bytes_sent | bytes }} + + + + + TLS Version: + {{ tls.Version }} + + + TLS SNI: + {{ tls.SNI }} + + + + + TLS Certificate: + {{ firstChain[0].Subject }} by {{ firstChain[0].Issuer }} + + + Trust-Chain + +
    +
  1. + {{ cert.Subject }} by {{ cert.Issuer }} +
  2. +
+
+
+
+
+
+
+ + +
+ + Domain: + {{dns.Domain}} + + + Query: + {{dns.Question}} + + + + Response: + {{dns.RCode}} + + + + Served from Cache: + {{dns.ServedFromCache ? 'yes' : 'no'}} + + + + Expires: + {{dns.Expires | date:'medium'}} + +
+
+ +
+ + Domain: + + + + + + Scope: + + Internet Peer-to-Peer + Internet Multicast + Device-Local + LAN Peer-to-Peer + LAN Multicast + LAN Peer-to-Peer + + N/A + N/A + N/A + + + {{ conn.direction === 'inbound' ? ' Incoming' : ' Outgoing'}} + + + + Remote Peer: + + + {{ conn.remote_ip || 'DNS Request'}} + {{ ':'+conn.remote_port }} + + + + Country: + {{ conn.country || 'N/A'}} + + + ASN: + {{ conn.asn || 'N/A' }} + + + AS Org: + {{ conn.as_owner || 'N/A' }} + +
+ +
+ + Binary Path: + {{ conn.path }} + + + Reason: + + {{conn.extra_data?.reason?.Msg}} + + + + Applied Setting: + + {{ helper.settings[option] || '' }}  +  from {{ + !!conn.extra_data?.reason?.Profile ? "App" : + "Global" }} Settings + + + + +
+ +
+

SPN Tunnel

+ + + This connection has not been routed through the Safing Privacy Network. + + + +
+
+ + + + +
+
+ + Path Costs: + {{ conn.extra_data?.tunnel?.PathCost }} + + + Routing Algorithm: + {{ conn.extra_data?.tunnel?.RoutingAlg }} + +
+
+ + + The connection was routed through the Safing Privacy Network, but the tunnel information is not available. Try + reloading the connections. + +
+
+ +
+

Data Usage

+ +
+
+ +
+ + + + + + + + +
diff --git a/desktop/angular/src/app/shared/netquery/connection-details/conn-details.scss b/desktop/angular/src/app/shared/netquery/connection-details/conn-details.scss new file mode 100644 index 00000000..f850b003 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/connection-details/conn-details.scss @@ -0,0 +1,114 @@ +:host { + section { + display: grid; + + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + + width: 100%; + overflow: hidden; + gap: 1.5rem; + } +} + +section { + &>div { + @apply flex flex-col gap-2 items-start justify-start text-xxs; + + &>span { + @apply space-x-1 text-ellipsis block overflow-hidden w-full; + + &>span:first-child { + @apply text-secondary whitespace-nowrap; + } + + &>span:last-child { + @apply whitespace-nowrap; + } + } + } +} + + +.tunnel-path { + position: relative; + + .line { + position: absolute; + top: 10px; + bottom: 10px; + left: 8px; + width: 1px; + background-color: rgba(255, 255, 255, 0.1); + } + + .node-tag { + border-radius: 1px solid rgba(255, 255, 255, 0.2); + background-color: rgba(255, 255, 255, 0.1); + padding: 2px; + font-size: 85%; + border-radius: 2px; + transform: scale(0.85); + } + + ul { + position: relative; + padding-left: 20px; + + li:not(:last-of-type) { + padding-bottom: 0.35rem; + } + + .ip { + margin-left: 0.35rem; + } + + .hop-icon { + display: inline-block; + margin-left: -17px; + margin-right: 4px; + font-weight: 400; + + &.country { + margin-left: -20px; + } + } + + .hop-title { + margin-right: 2px; + } + + .country { + display: inline-block; + margin-left: -20px; + margin-right: 4px; + + &.unknown { + height: 14px; + width: 16px; + position: relative; + top: 3px; + border: 1px solid rgba(0, 0, 0, 0.25); + opacity: 0.5; + border-radius: 3px; + @apply bg-buttons-icon; + } + } + } +} + + +@keyframes arrow_move { + 0% { + top: 0%; + opacity: 1; + } + + 85% { + opacity: 1; + } + + 100% { + top: 95%; + opacity: 0; + } +} diff --git a/desktop/angular/src/app/shared/netquery/connection-details/conn-details.ts b/desktop/angular/src/app/shared/netquery/connection-details/conn-details.ts new file mode 100644 index 00000000..78dfecda --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/connection-details/conn-details.ts @@ -0,0 +1,147 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, inject } from "@angular/core"; +import { BandwidthChartResult, ConnectionBandwidthChartResult, IPProtocol, IPScope, IsDenied, IsDNSRequest, Netquery, NetqueryConnection, PortapiService, Process, Verdict } from "@safing/portmaster-api"; +import { SfngDialogService } from '@safing/ui'; +import { Subscription } from "rxjs"; +import { ProcessDetailsDialogComponent } from '../../process-details-dialog'; +import { NetqueryHelper } from "../connection-helper.service"; +import { BytesPipe } from "../../pipes/bytes.pipe"; +import { formatDuration } from "../../pipes"; + + + +@Component({ + selector: 'sfng-netquery-conn-details', + styleUrls: ['./conn-details.scss'], + templateUrl: './conn-details.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SfngNetqueryConnectionDetailsComponent implements OnInit, OnDestroy, OnChanges { + helper = inject(NetqueryHelper) + private readonly portapi = inject(PortapiService) + private readonly dialog = inject(SfngDialogService) + private readonly cdr = inject(ChangeDetectorRef) + private readonly netquery = inject(Netquery) + + @Input() + conn: NetqueryConnection | null = null; + + process: Process | null = null; + + readonly IsDNS = IsDNSRequest; + readonly verdict = Verdict; + readonly Protocols = IPProtocol; + readonly scopes = IPScope; + private _subscription = Subscription.EMPTY; + + formatBytes = (n: d3.NumberValue, seriesKey?: string) => { + let prefix = ''; + if (seriesKey !== undefined) { + prefix = seriesKey === 'incoming' ? 'Received: ' : 'Sent: ' + } + return prefix + new BytesPipe().transform(n.valueOf()) + } + + formatTime = (n: Date) => { + const diff = Math.floor(new Date().getTime() - n.getTime()) + return formatDuration(diff, false, true) + " ago" + } + + tooltipFormat = (n: BandwidthChartResult) => { + const bytes = new BytesPipe().transform + const received = `Received: ${bytes(n?.incoming || 0)}`; + const sent = `Sent: ${bytes(n?.outgoing || 0)}` + + if ((n?.incoming || 0) > (n?.outgoing || 0)) { + return `${received}\n${sent}` + } + return `${sent}\n${received}` + } + + connectionNotice: string = ''; + bwData: ConnectionBandwidthChartResult[] = []; + + ngOnChanges(changes: SimpleChanges) { + if (!!changes?.conn) { + this.updateConnectionNotice(); + this.loadBandwidthChart(); + + if (this.conn?.extra_data?.pid !== undefined) { + this.portapi.get(`network:tree/${this.conn.extra_data.pid}-${this.conn.extra_data.processCreatedAt}`) + .subscribe({ + next: p => { + this.process = p; + this.cdr.markForCheck(); + }, + error: () => { + this.process = null; // the process does not exist anymore + this.cdr.markForCheck(); + } + }) + } else { + this.process = null; + } + } + } + + ngOnInit() { + this._subscription = this.helper.refresh.subscribe(() => { + this.updateConnectionNotice(); + this.loadBandwidthChart(); + + this.cdr.markForCheck(); + }) + } + + ngOnDestroy() { + this._subscription.unsubscribe(); + } + + openProcessDetails() { + this.dialog.create(ProcessDetailsDialogComponent, { + data: this.process, + backdrop: true, + autoclose: true, + }) + } + + private loadBandwidthChart() { + this.bwData = []; + + if (!this.conn) { + this.cdr.markForCheck() + + return; + } + + this.netquery.connectionBandwidthChart([this.conn!.id], 1) + .subscribe(result => { + if (!result[this.conn!.id]?.length) { + return; + } + + this.bwData = result[this.conn!.id]; + + this.cdr.markForCheck(); + }); + } + + private updateConnectionNotice() { + this.connectionNotice = ''; + if (!this.conn) { + return; + } + + if (this.conn!.verdict === Verdict.Failed) { + this.connectionNotice = 'Failed with previous settings.' + return; + } + + if (IsDenied(this.conn!.verdict)) { + this.connectionNotice = 'Blocked by previous settings.'; + } else { + this.connectionNotice = 'Allowed by previous settings.'; + } + + this.connectionNotice += ' You current settings could decide differently.' + } +} diff --git a/desktop/angular/src/app/shared/netquery/connection-details/index.ts b/desktop/angular/src/app/shared/netquery/connection-details/index.ts new file mode 100644 index 00000000..1740e308 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/connection-details/index.ts @@ -0,0 +1 @@ +export * from './conn-details'; diff --git a/desktop/angular/src/app/shared/netquery/connection-helper.service.ts b/desktop/angular/src/app/shared/netquery/connection-helper.service.ts new file mode 100644 index 00000000..fbe1b769 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/connection-helper.service.ts @@ -0,0 +1,537 @@ +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable, Renderer2, inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { AppProfile, AppProfileService, ConfigService, IPScope, NetqueryConnection, Pin, PossilbeValue, QueryResult, SPNService, Verdict, deepClone, flattenProfileConfig, getAppSetting, setAppSetting } from '@safing/portmaster-api'; +import { BehaviorSubject, Observable, OperatorFunction, Subject, combineLatest } from 'rxjs'; +import { distinctUntilChanged, filter, map, switchMap, take, takeUntil } from 'rxjs/operators'; +import { ActionIndicatorService } from '../action-indicator'; +import { objKeys } from '../utils'; +import { SfngSearchbarFields } from './searchbar'; +import { INTEGRATION_SERVICE } from 'src/app/integration'; + +export const IPScopeNames: { [key in IPScope]: string } = { + [IPScope.Invalid]: "Invalid", + [IPScope.Undefined]: "Undefined", + [IPScope.HostLocal]: "Device Local", + [IPScope.LinkLocal]: "Link Local", + [IPScope.SiteLocal]: "LAN", + [IPScope.Global]: "Internet", + [IPScope.LocalMulticast]: "LAN Multicast", + [IPScope.GlobalMulitcast]: "Internet Multicast" +} + +export interface LocalAppProfile extends AppProfile { + FlatConfig: { [key: string]: any } +} + +@Injectable() +export class NetqueryHelper { + readonly settings: { [key: string]: string } = {}; + + refresh = new Subject(); + + private onShiftKey$ = new BehaviorSubject(false); + private onCtrlKey$ = new BehaviorSubject(false); + private addToFilter$ = new Subject(); + private destroy$ = new Subject(); + private appProfiles$ = new BehaviorSubject([]); + private spnMapPins$ = new BehaviorSubject(null); + private readonly integration = inject(INTEGRATION_SERVICE); + + readonly onShiftKey: Observable; + readonly onCtrlKey: Observable; + + constructor( + private router: Router, + private profileService: AppProfileService, + private configService: ConfigService, + private actionIndicator: ActionIndicatorService, + private renderer: Renderer2, + private spnService: SPNService, + @Inject(DOCUMENT) private document: Document, + ) { + const cleanupKeyDown = this.renderer.listen(this.document, 'keydown', (event: KeyboardEvent) => { + if (event.shiftKey) { + this.onShiftKey$.next(true) + } + if (event.ctrlKey) { + this.onCtrlKey$.next(true); + } + }); + + const cleanupKeyUp = this.renderer.listen(this.document, 'keyup', () => { + this.onShiftKey$.next(false); + this.onCtrlKey$.next(false); + }) + + const windowBlur = this.renderer.listen(window, 'blur', () => { + this.onShiftKey$.next(false); + this.onCtrlKey$.next(false); + }) + + this.destroy$.subscribe({ + complete: () => { + cleanupKeyDown(); + cleanupKeyUp(); + windowBlur(); + } + }) + + this.onShiftKey = this.onShiftKey$ + .pipe(distinctUntilChanged()); + + this.onCtrlKey = this.onCtrlKey$ + .pipe(distinctUntilChanged()); + + this.configService.query('') + .subscribe(settings => { + settings.forEach(setting => { + this.settings[setting.Key] = setting.Name; + }); + this.refresh.next(); + }); + + // watch all application profiles + this.profileService.watchProfiles() + .pipe(takeUntil(this.destroy$)) + .subscribe(profiles => { + this.appProfiles$.next((profiles || []).map(p => { + return { + ...p, + FlatConfig: flattenProfileConfig(p.Config), + } + })) + }); + + this.spnService.watchPins() + .pipe(takeUntil(this.destroy$)) + .subscribe(pins => { + this.spnMapPins$.next(pins); + }) + } + + decodePrettyValues(field: keyof NetqueryConnection, values: any[]): any[] { + if (field === 'verdict') { + return values.map(val => Verdict[val]).filter(value => value !== undefined); + } + + if (field === 'scope') { + return values.map(val => { + // check if it's a value of the IPScope enum + const scopeValue = IPScope[val]; + if (!!scopeValue) { + return scopeValue; + } + + // otherwise check if it's pretty name of the scope translation + val = `${val}`.toLocaleLowerCase(); + return objKeys(IPScopeNames).find(scope => IPScopeNames[scope].toLocaleLowerCase() === val) + }).filter(value => value !== undefined); + } + + if (field === 'allowed') { + return values.map(val => { + if (typeof val !== 'string') { + return val + } + + switch (val.toLocaleLowerCase()) { + case 'yes': + return true + case 'no': + return false + case 'n/a': + case 'null': + return null + default: + return val + } + }) + } + + if (field === 'exit_node') { + const lm = new Map(); + (this.spnMapPins$.getValue() || []) + .forEach(pin => lm.set(pin.Name, pin)); + + return values.map(val => lm.get(val)?.ID || val) + } + + return values; + } + + attachProfile(): OperatorFunction { + return source => combineLatest([ + source, + this.appProfiles$, + ]).pipe( + map(([items, profiles]) => { + let lm = new Map(); + profiles.forEach(profile => { + lm.set(`${profile.Source}/${profile.ID}`, profile) + }) + + return items.map(item => { + if ('profile' in item) { + item.__profile = lm.get(item.profile!) + } + + return item; + }) + }) + ) + } + + attachPins(): OperatorFunction { + return source => combineLatest([ + source, + this.spnMapPins$ + .pipe( + filter(result => result !== null), + take(1), + ), + ]).pipe( + map(([items, pins]) => { + let lm = new Map(); + pins!.forEach(pin => { + lm.set(pin.ID, pin) + }) + + return items.map(item => { + if ('exit_node' in item) { + item.__exitNode = lm.get(item.exit_node!) + } + + return item; + }) + }) + ) + } + + encodeToPossibleValues(field: string): OperatorFunction { + return source => combineLatest([ + source, + this.appProfiles$, + this.spnMapPins$, + ]).pipe( + map(([items, profiles, pins]) => { + // convert profile IDs to profile name + if (field === 'profile') { + let lm = new Map(); + profiles.forEach(profile => { + lm.set(`${profile.Source}/${profile.ID}`, profile) + }) + + return items.map((item: any) => { + const profile = lm.get(item.profile!) + return { + Name: profile?.Name || `${item.profile}`, + Value: item.profile!, + Description: '', + __profile: profile || null, + ...item, + } + }) + } + + // convert verdict identifiers to their pretty name. + if (field === 'verdict') { + return items.map(item => { + if (Verdict[item.verdict!] === undefined) { + return null + } + + return { + Name: Verdict[item.verdict!], + Value: item.verdict, + Description: '', + ...item + } + }) + } + + // convert the IP scope identifier to a pretty name + if (field === 'scope') { + return items.map(item => { + if (IPScope[item.scope!] === undefined) { + return null + } + + return { + Name: IPScopeNames[item.scope!], + Value: item.scope, + Description: '', + ...item + } + }) + } + + if (field === 'allowed') { + return items + // we remove any "null" value from allowed here as it may happen for a really short + // period of time and there's no reason to actually filter for them because + // from showing a "null" value to the user clicking it the connection will have been + // verdicted and thus no results will show up for "null". + .filter(item => typeof item.allowed === 'boolean') + .map(item => { + return { + Name: item.allowed ? 'Yes' : 'No', + Value: item.allowed, + Description: '', + ...item + } + }) + } + + if (field === 'exit_node') { + const lm = new Map(); + pins!.forEach(pin => lm.set(pin.ID, pin)); + + return items.map(item => { + const pin = lm.get(item.exit_node!); + return { + Name: pin?.Name || item.exit_node, + Value: item.exit_node, + Description: 'Operated by ' + (pin?.VerifiedOwner || 'N/A'), + ...item + } + }) + } + + // the rest is just converted into the {@link PossibleValue} form + // by using the value as the "Name". + return items.map(item => ({ + Name: `${item[field]}`, + Value: item[field], + Description: '', + ...item, + })) + }), + // finally, remove any values that have been mapped to null in the above stage. + // this may happen for values that are not valid for the given model field (i.e. using "Foobar" for "verdict") + map(results => { + return results.filter(val => !!val) + }) + ) + } + + dispose() { + this.onShiftKey$.complete(); + + this.destroy$.next(); + this.destroy$.complete(); + } + + /** Emits added fields whenever addToFilter is called */ + onFieldsAdded(): Observable { + return this.addToFilter$.asObservable(); + } + + /** Adds a new filter to the current query */ + addToFilter(key: string, value: any[]) { + this.addToFilter$.next({ + [key]: value, + }) + } + + /** + * @private + * Returns the class used to color the connection's + * verdict. + * + * @param conn The connection object + */ + getVerdictClass(conn: NetqueryConnection): string { + return Verdict[conn.verdict]?.toLocaleLowerCase() || `unknown-verdict<${conn.verdict}>`; + } + + /** + * @private + * Redirect the user to a settings key in the application + * profile. + * + * @param key The settings key to redirect to + */ + redirectToSetting(setting: string, conn: NetqueryConnection, globalSettings = false) { + const reason = conn.extra_data?.reason; + if (!reason) { + return; + } + + if (!setting) { + setting = reason.OptionKey; + } + + if (!setting) { + return; + } + + if (globalSettings) { + this.router.navigate( + ['/', 'settings'], { + queryParams: { + setting: setting, + } + }) + return; + } + + let profile = conn.profile + + if (!!reason.Profile) { + profile = reason.Profile; + } + + if (profile.startsWith("core:profiles/")) { + profile = profile.replace("core:profiles/", "") + } + + this.router.navigate( + ['/', 'app', ...profile.split("/")], { + queryParams: { + tab: 'settings', + setting: setting, + } + }) + } + + /** + * @private + * Redirect the user to "outgoing rules" setting in the + * application profile/settings. + */ + redirectToRules(conn: NetqueryConnection) { + if (conn.direction === 'inbound') { + this.redirectToSetting('filter/serviceEndpoints', conn); + } else { + this.redirectToSetting('filter/endpoints', conn); + } + } + + /** + * @private + * Dump a connection to the console + * + * @param conn The connection to dump + */ + async dumpConnection(conn: NetqueryConnection) { + // Copy to clip-board if supported + try { + await this.integration.writeToClipboard(JSON.stringify(conn, undefined, " ")) + this.actionIndicator.info("Copied to Clipboard") + } catch (err: any) { + this.actionIndicator.error("Copy to Clipboard Failed", err?.message || JSON.stringify(err)) + } + } + + /** + * @private + * Creates a new "block domain" outgoing rules + */ + blockAll(domain: string, conn: NetqueryConnection) { + /* Deactivate until exact behavior is specified. + if (this.isDomainBlocked(domain)) { + this.actionIndicator.info(domain + ' already blocked') + return; + } + */ + + domain = domain.replace(/\.+$/, ''); + const newRule = `- ${domain}`; + this.updateRules(newRule, true, conn) + } + + /** + * @private + * Removes a "block domain" rule from the outgoing rules + */ + unblockAll(domain: string, conn: NetqueryConnection) { + /* Deactivate until exact behavior is specified. + if (!this.isDomainBlocked(domain)) { + this.actionIndicator.info(domain + ' already allowed') + return; + } + */ + + domain = domain.replace(/\.+$/, ''); + const newRule = `+ ${domain}`; + this.updateRules(newRule, true, conn); + } + + /** + * Updates the outgoing rule set and either creates or deletes + * a rule. If a rule should be created but already exists + * it is moved to the top. + * + * @param newRule The new rule to create or delete. + * @param add Whether or not to create or delete the rule. + */ + private updateRules(newRule: string, add: boolean, conn: NetqueryConnection) { + if (!conn.profile) { + return + } + + let key = 'filter/endpoints'; + if (conn.direction === 'inbound') { + key = 'filter/serviceEndpoints' + } + + this.profileService.getAppProfile(conn.profile) + .pipe( + switchMap(profile => { + let rules = getAppSetting(profile.Config, key) || []; + rules = rules.filter(rule => rule !== newRule); + + if (add) { + rules.splice(0, 0, newRule) + } + + const newProfile = deepClone(profile); + + if (newProfile.Config === null || newProfile.Config === undefined) { + newProfile.Config = {} + } + + setAppSetting(newProfile.Config, key, rules); + + return this.profileService.saveProfile(newProfile) + }) + ) + .subscribe({ + next: () => { + if (add) { + this.actionIndicator.success('Rules Updated', 'Successfully created a new rule.') + } else { + this.actionIndicator.success('Rules Updated', 'Successfully removed matching rule.') + } + }, + error: err => { + this.actionIndicator.error('Failed to update rules', JSON.stringify(err)) + } + }); + } + + /** + * Iterates of all outgoing rules and collects which domains are blocked. + * It stops collecting domains as soon as the first "allow something" rule + * is hit. + */ + // FIXME + /* + private collectBlockedDomains() { + let blockedDomains = new Set(); + + const rules = getAppSetting(this.profile!.profile!.Config, 'filter/endpoints') || []; + for (let i = 0; i < rules.length; i++) { + const rule = rules[i]; + if (rule.startsWith('+ ')) { + break; + } + + blockedDomains.add(rule.slice(2)) + } + + this.blockedDomains = Array.from(blockedDomains) + } + */ +} diff --git a/desktop/angular/src/app/shared/netquery/connection-row/conn-row.html b/desktop/angular/src/app/shared/netquery/connection-row/conn-row.html new file mode 100644 index 00000000..3c721b0b --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/connection-row/conn-row.html @@ -0,0 +1,146 @@ +
+ + + + + + + + + + + + + + + Internet Peer-to-Peer + Internet Multicast + Device-Local + LAN Peer-to-Peer + LAN Multicast + LAN Peer-to-Peer + + N/A + N/A + N/A + + + {{ conn.direction === 'inbound' ? ' Incoming' : ' Outgoing'}} + + +
+ + +
+ + {{ conn.country | countryName }} +
+
+ + + +
+ + + {{ conn.__profile.Name }} + +
+ +
+ + + + + {{ conn.remote_ip }} :{{ conn.remote_port }} + + + + DNS Request + + +
+ + + + + +
+ + + + + App Setting + + + + Global Setting + + + + + + + Allow {{ conn.domain ? 'Domain' : 'IP'}} + + + + + Block {{ conn.domain ? 'Domain' : 'IP '}} + + + + + Copy JSON + +
diff --git a/desktop/angular/src/app/shared/netquery/connection-row/conn-row.scss b/desktop/angular/src/app/shared/netquery/connection-row/conn-row.scss new file mode 100644 index 00000000..0d737830 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/connection-row/conn-row.scss @@ -0,0 +1,43 @@ +:host { + @apply w-full flex-grow gap-4 grid justify-start items-center overflow-hidden; + + grid-template-columns: + 1fr 1fr 1fr 2rem; + + grid-auto-rows: 1.5rem; + grid-template-rows: none; + + &>* { + @apply overflow-hidden whitespace-nowrap; + + &>*:last-child { + @apply overflow-hidden text-ellipsis; + } + } + + --app-icon-size: 20px; +} + +:host-context(.min-width-768px) { + :host { + grid-template-columns: + 1fr 4rem 1fr 1fr 5rem 2rem; + ; + } +} + +:host-context(.min-width-1024px) { + :host { + grid-template-columns: + 1fr 4rem 1fr 1fr 5rem 0.5fr 2rem; + ; + } +} + +:host-context(.min-width-1280px) { + :host { + grid-template-columns: + 1fr 4rem 1fr 1fr 8rem 1fr 2rem; + ; + } +} diff --git a/desktop/angular/src/app/shared/netquery/connection-row/conn-row.ts b/desktop/angular/src/app/shared/netquery/connection-row/conn-row.ts new file mode 100644 index 00000000..b841d116 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/connection-row/conn-row.ts @@ -0,0 +1,78 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from "@angular/core"; +import { AppProfile, IPScope, NetqueryConnection, Verdict } from "@safing/portmaster-api"; +import { interval, Subscription } from "rxjs"; +import { share, startWith } from "rxjs/operators"; +import { NetqueryHelper } from "../connection-helper.service"; + +interface ProfileAttachedConnection extends NetqueryConnection { + __profile?: AppProfile; +} + +@Component({ + selector: 'sfng-netquery-connection-row', + templateUrl: './conn-row.html', + styleUrls: [ + './conn-row.scss' + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SfngNetqueryConnectionRowComponent implements OnInit, OnDestroy { + readonly scopes = IPScope; + readonly verdicts = Verdict; + + @Input() + set conn(c: ProfileAttachedConnection) { + this._conn = c; + } + get conn() { return this._conn; } + _conn!: ProfileAttachedConnection; + + @Input() + activeRevision: number | undefined = 0; + + get isOutdated() { + // FIXME(ppacher) + return false; + /* + if (!this.conn || !this.helper.profile) { + return false; + } + if (this.helper.profile.currentProfileRevision === -1) { + // we don't know the revision counter yet ... + return false; + } + return this.conn.profile_revision !== this.helper.profile.currentProfileRevision; + */ + } + + /* timeAgoTicker ticks every 10000 seconds to force a refresh + of the timeAgo pipes */ + timeAgoTicker: number = 0; + + private _subscription = Subscription.EMPTY; + + constructor( + public helper: NetqueryHelper, + private changeDetectorRef: ChangeDetectorRef, + ) { } + + ngOnInit() { + this._subscription = new Subscription(); + + const tickerSub = interval(10000).pipe( + startWith(-1), + share() + ).subscribe(i => this.timeAgoTicker = i); + + const helperSub = this.helper.refresh.subscribe(() => { + this.changeDetectorRef.markForCheck(); + }) + + this._subscription.add(helperSub); + this._subscription.add(tickerSub); + } + + ngOnDestroy() { + this._subscription.unsubscribe(); + } +} diff --git a/desktop/angular/src/app/shared/netquery/connection-row/index.ts b/desktop/angular/src/app/shared/netquery/connection-row/index.ts new file mode 100644 index 00000000..adbe1933 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/connection-row/index.ts @@ -0,0 +1 @@ +export * from './conn-row'; diff --git a/desktop/angular/src/app/shared/netquery/index.ts b/desktop/angular/src/app/shared/netquery/index.ts new file mode 100644 index 00000000..84660587 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/index.ts @@ -0,0 +1,2 @@ +export * from './netquery.component'; +export * from './netquery.module'; diff --git a/desktop/angular/src/app/shared/netquery/line-chart/index.ts b/desktop/angular/src/app/shared/netquery/line-chart/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/desktop/angular/src/app/shared/netquery/line-chart/line-chart.ts b/desktop/angular/src/app/shared/netquery/line-chart/line-chart.ts new file mode 100644 index 00000000..5a12cb9d --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/line-chart/line-chart.ts @@ -0,0 +1,592 @@ +import { coerceBooleanProperty, coerceNumberProperty, coerceStringArray } from '@angular/cdk/coercion'; +import { AfterViewInit, ChangeDetectionStrategy, Component, DestroyRef, ElementRef, Input, OnChanges, OnInit, SimpleChanges, inject } from '@angular/core'; +import { BandwidthChartResult, ChartResult } from '@safing/portmaster-api'; +import * as d3 from 'd3'; +import { Selection } from 'd3'; +import { AppComponent } from 'src/app/app.component'; +import { formatDuration, timeAgo } from '../../pipes'; +import { objKeys } from '../../utils'; +import { BytesPipe } from '../../pipes/bytes.pipe'; + +export interface SeriesConfig { + lineColor: string; + areaColor?: string; +} + +export interface Marker { + text: string; + time: Date | number | string; +} + +export interface ChartConfig { + series: { + [key in Exclude]?: SeriesConfig; + }, + time?: { + from: number | string | Date; + to?: number | string | Date; + }, + fromMargin?: number; + toMargin?: number; + valueFormat?: (n: d3.NumberValue, seriesKey?: string) => string, + tooltipFormat?: (data: T) => string; + timeFormat?: (n: Date) => string, + showDataPoints?: boolean; + fillEmptyTicks?: { + interval: number; + }, + verticalMarkers?: Marker[]; +} + +function coerceDate(d: Date | number | string): Date { + if (typeof d === 'string') { + return new Date(d) + } + + if (d instanceof Date) { + return d + } + + if (d < 0) { + return new Date((new Date()).getTime() + d * 1000) + } + + return new Date(d * 1000); +} + +export const DefaultChartConfig: ChartConfig = { + series: { + value: { + lineColor: 'text-green-200', + areaColor: 'text-green-100 text-opacity-25' + }, + countBlocked: { + lineColor: 'text-red-200', + areaColor: 'text-red-100 text-opacity-25' + } + }, +} + +export const DefaultBandwidthChartConfig: ChartConfig> = { + series: { + outgoing: { + lineColor: 'text-deepPurple-500', + areaColor: 'text-deepPurple-700 text-opacity-5', + }, + incoming: { + lineColor: 'text-cyan-800', + areaColor: 'text-cyan-700 text-opacity-5', + }, + }, + time: { + from: -10 * 60, + }, + valueFormat: (n: d3.NumberValue, seriesKey?: string) => { + let prefix = ''; + if (seriesKey !== undefined) { + prefix = seriesKey === 'incoming' ? 'Received: ' : 'Sent: ' + } + return prefix + new BytesPipe().transform(n.valueOf()) + }, + timeFormat: (n: Date) => { + const diff = Math.floor(new Date().getTime() - n.getTime()) + return formatDuration(diff, false, true) + " ago" + }, + tooltipFormat: (n: BandwidthChartResult) => { + const bytes = new BytesPipe().transform + const received = `Received: ${bytes(n?.incoming || 0)}`; + const sent = `Sent: ${bytes(n?.outgoing || 0)}` + + if ((n?.incoming || 0) > (n?.outgoing || 0)) { + return `${received}\n${sent}` + } + return `${sent}\n${received}` + }, + showDataPoints: true, + fillEmptyTicks: { + interval: 60 + }, +} + +export interface SeriesData { + timestamp: number; +} + +@Component({ + selector: 'sfng-netquery-line-chart', + styles: [ + ` + :host { + @apply block h-full w-full; + } + ` + ], + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SfngNetqueryLineChartComponent implements OnChanges, OnInit, AfterViewInit { + private destroyRef = inject(DestroyRef); + + @Input() + data: D[] = []; + + private preparedData: D[] = []; + + private width = 700; + private height = 250; + + @Input() + set margin(v: any) { + this._margin = coerceNumberProperty(v); + } + get margin() { return this._margin; } + private _margin = 0; + + @Input() + config!: ChartConfig; + + svg!: Selection; + svgInner!: Selection; + yScale!: d3.ScaleLinear; + xScale!: d3.ScaleTime; + xAxis!: Selection; + yAxis!: Selection; + + @Input() + set showAxis(v: any) { + this._showAxis = coerceBooleanProperty(v); + } + get showAxis() { + return this._showAxis; + } + private _showAxis = true; + + constructor( + public chartElem: ElementRef, + private app: AppComponent + ) { } + + ngOnInit() { + if (!this.config) { + this.config = DefaultChartConfig as any; + } + + const observer = new ResizeObserver(() => { + this.redraw(); + }) + + observer.observe(this.chartElem.nativeElement) + + this.destroyRef.onDestroy(() => observer.disconnect()) + + } + + ngAfterViewInit(): void { + requestAnimationFrame(() => { + this.redraw() + }) + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.hasOwnProperty('config') && this.config) { + this.redraw() + return + } + + if (changes.hasOwnProperty('data') && this.data) { + this.drawChart(); + } + } + + get yMargin() { + if (this.showAxis) { + return 16; + } + return 0; + } + + redraw(event?: Event) { + if (!!this.svg) { + this.svg.remove(); + } + + this.initializeChart(); + this.drawChart(); + } + + private initializeChart(): void { + this.width = this.chartElem.nativeElement.getBoundingClientRect().width; + this.height = this.chartElem.nativeElement.getBoundingClientRect().height; + + this.svg = d3 + .select(this.chartElem.nativeElement) + .append('svg') + + this.svg.attr('width', this.width); + this.svg.attr('height', this.height); + + this.svgInner = this.svg + .append('g') + .attr('height', '100%'); + + this.yScale = d3 + .scaleLinear() + + this.xScale = d3.scaleTime(); + + // setup event handlers to higlight the closest data points + let lastClosestIndex = -1; + + if (this.config.showDataPoints) { + const self = this; + this.svg + .on("mousemove", function (event: MouseEvent) { + let x = d3.pointer(event)[0]; + + let closest = self.data.reduce((best, value, idx) => { + let absx = Math.abs(self.xScale(new Date(value.timestamp * 1000)) - x); + if (absx < best.value) { + return { index: idx, value: absx, timestamp: self.data[idx].timestamp } + } + + return best + + }, { index: 0, value: Number.MAX_SAFE_INTEGER, timestamp: 0 }) + + if (lastClosestIndex === closest.index) { + return; + } + lastClosestIndex = closest.index; + + if (self.config.tooltipFormat) { + // append a title to the parent SVG, this is a quick-fix for showing some + // information on the highlighted points + // TODO(ppacher): actually render a nice tooltip there. + let tooltip = self.svg + .select('title.tooltip') + + if (tooltip.empty()) { + tooltip = self.svg.append("title") + .attr("class", "tooltip") + } + + tooltip + .text(self.config.tooltipFormat!(self.data[closest.index])) + } + + self.svgInner + .select(".vertical-marker") + .selectAll(".mouse-position") + .remove() + + self.svgInner + .select(".vertical-marker") + .append("line") + .classed("mouse-position", true) + .attr("x1", d => self.xScale(closest.timestamp * 1000)) + .attr("y1", -10) + .attr("x2", d => self.xScale(closest.timestamp * 1000)) + .attr("y2", self.height - self.yMargin) + .classed("text-secondary text-opacity-50", true) + .attr("stroke", "currentColor") + .attr("stroke-width", 1) + .attr("stroke-dasharray", 2) + + self.svgInner + .select(".points") + .selectAll("circle") + .classed("opacity-100", d => self.xScale.invert(d[0]).getTime() === closest.timestamp * 1000) + }) + .on("mouseleave", function () { + lastClosestIndex = -1; + + self.svg.select("title.tooltip") + .remove() + + self.svg.select("line.mouse-position") + .remove() + + self.svgInner + .select(".points") + .selectAll("circle") + .attr("r", 4) + .classed("opacity-100", false) + }) + } + + objKeys(this.config.series).forEach(seriesKey => { + const seriesConfig = this.config.series[seriesKey]!; + + if (seriesConfig.areaColor) { + this.svgInner + .append('path') + .attr("fill", "currentColor") + .attr("class", `area-${String(seriesKey)} ${(seriesConfig.areaColor || '')}`) + } + + this.svgInner + .append('g') + .append('path') + .style('fill', 'none') + .style('stroke', 'currentColor') + .style('stroke-width', '1') + .attr('class', `line-${String(seriesKey)} ${seriesConfig.lineColor}`) + }) + + this.svgInner.append("g") + .attr("class", "vertical-marker") + + this.svgInner.append("g") + .attr("class", "points") + + if (this.showAxis) { + this.yAxis = this.svgInner + .append('g') + .attr('id', 'y-axis') + .attr('class', 'text-secondary text-opacity-75 ') + .style('transform', 'translate(' + (this.width - this.yMargin) + 'px, 0)'); + + this.xAxis = this.svgInner + .append('g') + .attr('id', 'x-axis') + .attr('class', 'text-secondary text-opacity-50 ') + .style('transform', 'translate(0, ' + (this.height - this.yMargin) + 'px)'); + } + } + + private getTimeRange(): { from: Date, to: Date } { + const time = { + from: this.data[0]?.timestamp || 0, + to: this.data[this.data.length - 1]?.timestamp || 0, + }; + + if (!!this.config.time) { + time.from = coerceDate(this.config.time.from).getTime() / 1000 + + if (this.config.fromMargin) { + time.from = time.from - this.config.fromMargin + } + + if (this.config.time.to) { + time.to = coerceDate(this.config.time.to).getTime() / 1000 + + if (this.config.toMargin) { + time.to = time.to + this.config.toMargin + } + } + } + + return { + from: new Date(time.from * 1000), + to: new Date(time.to * 1000) + }; + } + + private prepareDataSet(data: D[], time: { from: Date, to: Date }) { + const toTimestamp = Math.round(time.to.getTime() / 1000) + const fromTimestamp = Math.round(time.from.getTime() / 1000) + + // first, filter out all elements that are before or after the to date + data = data.filter(d => { + return d.timestamp >= fromTimestamp && d.timestamp <= toTimestamp + }) + + // check if we need to fill empty ticks + if (!this.config.fillEmptyTicks) { + return data; + } + + const interval = this.config.fillEmptyTicks.interval; + + let filledData: D[] = []; + const addEmpty = (ts: number) => { + let empty: any = { + timestamp: ts, + } + + Object.keys(this.config.series) + .forEach(s => empty[s] = 0) + + filledData.push(empty) + } + + let firstElement = data[0]!.timestamp; + if (this.config.time?.from) { + firstElement = Math.round(coerceDate(this.config.time.from).getTime() / 1000) + } + + // add empty values for the start-time until the first element / or the start tme + let lastTimeStamp = fromTimestamp - interval; + for (let ts = lastTimeStamp; ts <= firstElement; ts += interval) { + addEmpty(ts) + } + + // add emepty vaues for each missing tick during the dataset + lastTimeStamp = firstElement; + for (let idx = 0; idx < data.length; idx++) { + const elem = data[idx] + const elemTs = elem.timestamp; + + for (let ts = lastTimeStamp + interval; ts < elemTs; ts += interval) { + addEmpty(ts) + } + + filledData.push(elem) + lastTimeStamp = elemTs + } + + // if there's a specified end-time, add empty ticks from the last datapoint + // to the end-time + if (this.config.time?.to) { + for (let ts = lastTimeStamp + interval; ts <= toTimestamp; ts += interval) { + addEmpty(ts) + } + } + + return filledData + } + + private drawChart(): void { + if (!this.svg) { + return; + } + + if (!this.data?.length) { + return; + } + + this.data.sort((a, b) => a.timestamp - b.timestamp) + + // determine the time range that should be displayed. + const time = this.getTimeRange(); + + // fill empty ticks depending on the configuration. + this.preparedData = this.prepareDataSet(this.data, time) + + this.xScale + .range([0, this.width - this.yMargin]) + .domain([time.from, time.to]); + + this.yScale + .range([0, this.height - this.yMargin]) + .domain([ + d3.max(this.preparedData.map(d => { + return d3.max( + objKeys(this.config.series) + .map(series => { + return d[series] as number + }) + )! + }))! * 1.3, // 30% margin to top + 0 + ]) + + if (this.showAxis) { + const xAxis = d3 + .axisBottom(this.xScale) + .ticks(5) + .tickFormat((val, idx) => { + if (!!this.config.timeFormat) { + return this.config.timeFormat(val as any) + } + return timeAgo(val as any); + }) + + this.xAxis.call(xAxis); + + const yAxis = d3 + .axisLeft(this.yScale) + .ticks(2) + .tickFormat(d => ((this.config.valueFormat || this.yScale.tickFormat(2)) as any)(d, undefined)) + + this.yAxis.call(yAxis); + } + + const line = d3 + .line() + .x(d => d[0]) + .y(d => d[1]) + .curve(d3.curveMonotoneX); + + // define the area + const area = d3.area() + .x(d => d[0]) + .y0(this.height - this.yMargin) + .y1(d => d[1]) + .curve(d3.curveMonotoneX) + + // render vertical markers + const markers = (this.config.verticalMarkers || []) + .filter(marker => !!marker.time) + .map(marker => ({ + text: marker.text, + time: coerceDate(marker.time) + })); + + this.svgInner.select('.vertical-marker') + .selectAll("line.marker") + .data(markers) + .join("line") + .classed("marker", true) + .attr("x1", d => this.xScale(d.time)) + .attr("y1", -10) + .attr("x2", d => this.xScale(d.time)) + .attr("y2", this.height - this.yMargin) + .classed("text-secondary text-opacity-50", true) + .attr("stroke", "currentColor") + .attr("stroke-width", 3) + .attr("stroke-dasharray", 4) + .append("title") + .text(d => d.text) + + // FIXME(ppacher): somehow d3 does not recognize which data points must be removed + // or re-placed. For now, just remove them all + this.svgInner + .select('.points') + .selectAll("circle") + .remove() + + objKeys(this.config.series) + .forEach(seriesKey => { + const config = this.config.series[seriesKey]!; + + let points: [number, number][] = this.preparedData + .map(d => [ + this.xScale(new Date(d.timestamp * 1000)), + this.yScale((d as any)[seriesKey] || 0), + ]) + + let data: [number, number][] = this.preparedData + .map(d => [ + this.xScale(new Date(d.timestamp * 1000)), + this.yScale((d as any)[seriesKey] || 0), + ]) + + if (config.areaColor) { + this.svgInner.selectAll(`.area-${String(seriesKey)}`) + .data([data]) + .attr('d', area(data)) + } + + this.svgInner.select(`.line-${String(seriesKey)}`) + .attr('d', line(data)) + + if (this.config?.showDataPoints) { + this.svgInner + .select('.points') + .selectAll(`circle.point-${String(seriesKey)}`) + .data(points) + .enter() + .append("circle") + .classed(`points-${String(seriesKey)}`, true) + .attr("r", "4") + .attr("fill", "currentColor") + .attr("class", `opacity-0 ${config.lineColor}`) + .attr("cx", d => d[0]) + .attr("cy", d => d[1]) + .append("title") + .text(d => ((this.config.valueFormat || this.yScale.tickFormat(2)) as any)(this.yScale.invert(d[1]), String(seriesKey))) + } + }) + } +} diff --git a/desktop/angular/src/app/shared/netquery/netquery.component.html b/desktop/angular/src/app/shared/netquery/netquery.component.html new file mode 100644 index 00000000..8a05984b --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/netquery.component.html @@ -0,0 +1,388 @@ +
+ + + + +
+ + + +
+ + + Clear All + +
+ +
+ + +
+ +
+ + + + + + Loading ... + + + + + + + + + {{ value.Name || 'N/A' }} + + + + #{{ value.count }} connections + + + + + + + + + + + + {{ item.value || 'N/A' }} + + + + + + + + + + + + + + + +
+

+ Filter by {{ model.value!.menuTitle || model.key }} + +

+
    +
  • + + + {{ value.Name }} + + + + + + +
  • +
+
+
+
+ + +
+ +
+ Search History: + + + + + Quick Settings +
    +
  • + {{ qds.name }} +
  • +
+
+
+ +
+ + + + + {{ keyTranslation[value] || value }} + + + + + + + + + {{ keyTranslation[value] || value }} + + + + +
+
+ +
+
+

Connections

+
+ +
+
+ +
+

Data Usage

+
+ +
+
+ +
+ Loading Chart +
+
+ + +
+ + + + + + + + + + + + {{ keyTranslation[key] || key }} + + + {{ data[key] || 'N/A' }} + + + + + + {{ data.__profile.Name }} + + + + + + ( + + ) + + {{ data.__exitNode.Name }} + + + + + + {{ data[key] || 'N/A' }} + + + + + + + +
+
+ + + Use as filter + App Settings + + +
+ + + + + + +
+
+ + + + + +
+
+ +
+ {{ totalResultCount }} Results + of {{totalConnCount}} total connections + + + + Last Reload: {{ lastReload | timeAgo:(lastReloadTicker|async) }} + +
+ + + + + + +
+ + + + + +
+ All connections ended more than 10 minutes ago and have been removed. +
+ + + + +
+ +
+
+
+
+
+
+ + + + + + + + + +
+
+
+
+
+ + + +
+ + + + + + Loading connections ... +
+
+ +
+ + + + Pro Tip: + + + +
+ + + Press +
CTRL + Space
+ on any page to bring up the quick search box. +
+ + + Use your keyboard arrows to navigate through the search suggestions. Press +
ENTER
to search for the suggestion or use +
Shift + Enter
to add it to the search text. +
+ + + Inside the search box, use +
Ctrl + Space
to force loading suggestions. +
+ + + Use +
Shift + Click
to add connection attributes to the current filter. +
+ + + Hold +
Shift
to highlight attributes that can be used in the filter. +
+ + + Hold +
CTRL
and click attributes to copy them to the clipboard. +
diff --git a/desktop/angular/src/app/shared/netquery/netquery.component.ts b/desktop/angular/src/app/shared/netquery/netquery.component.ts new file mode 100644 index 00000000..d4befb47 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/netquery.component.ts @@ -0,0 +1,1270 @@ +import { coerceArray } from "@angular/cdk/coercion"; +import { FormatWidth, formatDate, getLocaleDateFormat, getLocaleId } from "@angular/common"; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, EventEmitter, Input, LOCALE_ID, OnDestroy, OnInit, Output, QueryList, TemplateRef, TrackByFunction, ViewChildren, inject, isDevMode } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute, Router } from "@angular/router"; +import { BandwidthChartResult, ChartResult, Condition, Database, FeatureID, GreaterOrEqual, IPScope, LessOrEqual, Netquery, NetqueryConnection, OrderBy, Pin, PossilbeValue, Query, QueryResult, SPNService, Select, Verdict } from "@safing/portmaster-api"; +import { Datasource, DynamicItemsPaginator, SelectOption } from "@safing/ui"; +import { BehaviorSubject, Observable, Subject, combineLatest, forkJoin, interval, of } from "rxjs"; +import { catchError, debounceTime, filter, map, share, skip, switchMap, take, takeUntil } from "rxjs/operators"; +import { ActionIndicatorService } from "../action-indicator"; +import { ExpertiseService } from "../expertise"; +import { objKeys } from "../utils"; +import { fadeInAnimation } from './../animations'; +import { IPScopeNames, LocalAppProfile, NetqueryHelper } from "./connection-helper.service"; +import { SfngSearchbarFields } from "./searchbar"; +import { SfngTagbarValue } from "./tag-bar"; +import { Parser } from "./textql"; +import { connectionFieldTranslation, mergeConditions } from "./utils"; +import { DefaultBandwidthChartConfig } from "./line-chart/line-chart"; +import { INTEGRATION_SERVICE } from "src/app/integration"; + +interface Suggestion extends PossilbeValue { + count: number; + selected?: boolean; +} + +interface Model { + suggestions: Suggestion[]; + searchValues: any[]; + visible: boolean | 'combinedMenu'; + menuTitle?: string; + loading: boolean; + tipupKey?: string; + virtual?: boolean; +} + +const freeTextSearchFields: (keyof Partial)[] = [ + 'domain', + 'as_owner', + 'path', + 'profile_name', +] + +const groupByKeys: (keyof Partial)[] = [ + 'domain', + 'as_owner', + 'country', + 'direction', + 'path', + 'profile' +] + +const orderByKeys: (keyof Partial)[] = [ + 'domain', + 'as_owner', + 'country', + 'direction', + 'path', + 'started', + 'ended', + 'profile', +] + +interface LocalQueryResult extends QueryResult { + _chart: Observable | null; + _group: Observable> | null; + __profile?: LocalAppProfile; + __exitNode?: Pin; +} + +interface QuickDateSetting { + name: string; + apply: () => [Date, Date]; +} + +/** + * Netquery Viewer + * + * This component is the actual viewer component for the netquery subsystem of the Portmaster. + * It allows the user to specify connection filters in multiple different ways and allows + * to do a deep-dive into all connections seen by the Portmaster (that are still stored in + * the in-memory SQLite database of the netquery subsystem). + * + * The user is able to modify the filter query by either: + * - using the available drop-downs + * - using the searchbar which + * - supports typed searches for connection fields (i.e. country:AT domain:google.at) + * - free-text search across the list of supported "full-text" search fields (see freeTextSearchFields) + * - by shift-clicking any value that has a SfngAddToFilter directive + * - by removing values from the tag bar. + */ + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'sfng-netquery-viewer', + templateUrl: './netquery.component.html', + providers: [ + NetqueryHelper, + ], + styles: [ + ` + :host { + @apply flex flex-col gap-3 pr-3 min-h-full; + } + + .protip pre { + @apply inline-block text-xxs uppercase rounded-sm bg-gray-500 bg-opacity-25 font-mono border-gray-500 border px-0.5; + } + ` + ], + animations: [ + fadeInAnimation + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +// eslint-disable-next-line @angular-eslint/component-class-suffix +export class SfngNetqueryViewer implements OnInit, OnDestroy, AfterViewInit { + /** @private Used to trigger a reload of the current filter */ + private search$ = new Subject(); + + /** @private The DestroyRef of the component, required for takeUntilDestroyed */ + private destroyRef = inject(DestroyRef); + + /** @private Used to trigger an update of all displayed values in the tag-bar. */ + private updateTagBar$ = new BehaviorSubject(undefined); + + /** @private Whether or not the next update on ActivatedRoute should be ignored */ + private skipNextRouteUpdate = false; + + /** @private Whether or not we should update the URL when performSearch() finishes */ + private skipUrlUpdate = false; + + /** @private The LOCALE_ID to format dates. */ + private localeId = inject(LOCALE_ID); + + private integration = inject(INTEGRATION_SERVICE); + + /** @private the date format for the nz-range-picker */ + dateFormat = getLocaleDateFormat(getLocaleId(this.localeId), FormatWidth.Medium) + + /** @private A list of quick-date settings for the nz-range-picker */ + quickDateSettings: QuickDateSetting[] = [ + { + name: 'Today', + apply: () => { + const now = new Date(); + return [ + new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0), + new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, -1), + ] + } + }, + { + name: 'Last 24 Hours', + apply: () => { + const now = new Date(); + return [ + new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours() - 24, now.getMinutes(), now.getSeconds()), + now + ] + } + }, + { + name: 'Last 7 Days', + apply: () => { + const now = new Date(); + return [ + new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7, now.getHours(), now.getMinutes(), now.getSeconds()), + now, + ] + } + }, + { + name: 'Last Month', + apply: () => { + const now = new Date(); + return [ + new Date(now.getFullYear(), now.getMonth() - 1, now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds()), + now, + ] + } + }, + ] + + applyQuickDateSetting(qds: QuickDateSetting) { + const [from, to] = qds.apply() + + const fromStr = formatDate(from, 'medium', this.localeId) + const toStr = formatDate(to, 'medium', this.localeId) + + this.onFieldsParsed({ + from: [fromStr], + to: [toStr] + }, true) + } + + /** @private - The paginator used for the result set */ + paginator!: DynamicItemsPaginator; + + /** @private - The total amount of connections without the filter applied */ + totalConnCount: number = 0; + + /** @private - The total amount of connections with the filter applied */ + totalResultCount: number = 0; + + /** The value of the free-text search */ + textSearch: string = ''; + + /** The date filter */ + dateFilter: Date[] = [] + + /** a list of allowed group-by keys */ + readonly allowedGroupBy = groupByKeys; + + /** a list of allowed order-by keys */ + readonly allowedOrderBy = orderByKeys; + + /** @private Whether or not we are currently loading data */ + loading = false; + + /** @private The connection chart data */ + connectionChartData: ChartResult[] = []; + + /** @private The bandwidth chart data */ + bwChartData: BandwidthChartResult[] = []; + + /** @private The configuration for the bandwidth chart */ + readonly bwChartConfig = DefaultBandwidthChartConfig; + + /** @private The list of "pro-tips" that are defined in the template. Only one pro-tip will be rendered depending on proTipIdx */ + @ViewChildren('proTip', { read: TemplateRef }) + proTips!: QueryList> + + /** @private The index of the pro-tip that is currently rendered. */ + proTipIdx = 0; + + /** @private The last time the connections were loaded */ + lastReload: Date = new Date(); + + /** @private Used to refresh the "Last reload xxx ago" message */ + lastReloadTicker = interval(2000) + .pipe( + takeUntilDestroyed(this.destroyRef), + map(() => Math.floor((new Date()).getTime() - this.lastReload.getTime()) / 1000), + share() + ) + + // whether or not the history database should be queried as well. + get useHistory() { + return this.dateFilter?.length; + } + + private get databases(): Database[] { + if (!this.useHistory) { + return [Database.Live]; + } + + return [Database.Live, Database.History]; + } + + // whether or not the current use has the history feature available. + canUseHistory$ = inject(SPNService).profile$ + .pipe( + map(profile => { + if (!profile) { + return false; + } + + return profile.current_plan?.feature_ids?.includes(FeatureID.History) || false; + }) + ); + + featureBw$ = inject(SPNService).profile$ + .pipe( + map(profile => { + if (!profile) { + return false; + } + + return profile.current_plan?.feature_ids?.includes(FeatureID.Bandwidth) || false; + }) + ); + + trackPageItem: TrackByFunction = (_, r) => { + if (this.groupByKeys?.length) { + return this.groupByKeys.map(key => r[key]).join('-') + } + return r.id! + } + + trackConnection: TrackByFunction = (_, c) => c.id + + constructor( + private netquery: Netquery, + private helper: NetqueryHelper, + private expertise: ExpertiseService, + private cdr: ChangeDetectorRef, + private actionIndicator: ActionIndicatorService, + private route: ActivatedRoute, + public router: Router, + ) { } + + @Input() + set filters(v: any | keyof this['models'] | (keyof this['models'])[]) { + v = coerceArray(v); + objKeys(this.models).forEach(key => { + // ignore any models that are marked as being shown in the combined-menu. + if (this.models[key]?.visible !== 'combinedMenu') { + this.models[key]!.visible = false; + } + }) + + v.forEach((val: any) => { + if (typeof val !== 'string') { + throw new Error("invalid value for @Input() filters") + } + + if (!this.isValidFilter(val)) { + throw new Error('invalid filter key ' + val) + } + + this.models[val]!.visible = true; + }) + } + + /** + * mergeFilter input can be used to apply an additional filter condition that cannot be modified by + * the user (like forcing a "profile" filter for the App View) + */ + @Input() + mergeFilter: Condition | null = null; + + /** The filter preset that will be used if no filter is configured otherwise */ + @Input() + filterPreset: string | null = null; + + @Output() + filterChange: EventEmitter = new EventEmitter(); + + /** @private Holds the value displayed in the tag-bar */ + tagbarValues: SfngTagbarValue[] = []; + + private updateDateRangeState() { + const values = [ + this.models.from.searchValues[0], + this.models.to.searchValues[0], + ] + + let fromValueTs = Date.parse(values[0]) + let toValueTs = Date.parse(values[1]) + + // if we failed to parse the date from a string, the user might + // just entered the timestamp in seconds + if (isNaN(fromValueTs)) { + fromValueTs = Number(values[0]) * 1000 + } + if (isNaN(toValueTs)) { + toValueTs = Number(values[1]) * 1000 + } + + const fromValid = !isNaN(fromValueTs) + const toValid = !isNaN(toValueTs) + + + let fromValue = new Date(fromValueTs) + let toValue = new Date(toValueTs); + + if (fromValid && toValid && fromValue.getTime() === toValue.getTime()) { + fromValue = new Date(fromValue.getFullYear(), fromValue.getMonth(), fromValue.getDate(), 0, 0, 0) + toValue = new Date(toValue.getFullYear(), toValue.getMonth(), toValue.getDate() + 1, 0, 0, -1) + } + + this.dateFilter = []; + + if (fromValid) { + this.dateFilter.push(fromValue) + this.models.from.searchValues = [ + formatDate(fromValue, 'medium', this.localeId) + ] + } + + if (toValid) { + if (!fromValid) { + this.dateFilter.push(new Date(2000, 0, 1)) + } + + this.dateFilter.push(toValue) + this.models.to.searchValues = [ + formatDate(toValue, 'medium', this.localeId) + ] + } + + this.cdr.markForCheck(); + } + + private getDateRangeCondition(): Condition | null { + this.updateDateRangeState() + + if (!this.dateFilter.length) { + return null + } + + const cond: GreaterOrEqual & Partial = { + $ge: Math.floor(this.dateFilter[0].getTime() / 1000), + } + + if (this.dateFilter.length >= 2) { + cond['$le'] = Math.floor(this.dateFilter[1].getTime() / 1000) + } + + return { + started: cond + } + } + + models: { [key: string]: Model } = initializeModels({ + domain: { + visible: true, + }, + as_owner: { + visible: true, + }, + country: { + visible: true, + }, + profile: { + visible: true + }, + allowed: { + visible: true, + }, + path: {}, + internal: {}, + type: {}, + encrypted: {}, + scope: { + visible: 'combinedMenu', + menuTitle: 'Network Scope', + suggestions: objKeys(IPScopeNames) + .sort() + .filter(key => key !== IPScope.Undefined) + .map(scope => { + return { + Name: IPScopeNames[scope], + Value: scope, + count: 0, + Description: '' + } + }) + }, + verdict: {}, + started: {}, + ended: {}, + profile_revision: {}, + remote_ip: {}, + remote_port: {}, + local_ip: {}, + local_port: {}, + ip_protocol: {}, + direction: { + visible: 'combinedMenu', + menuTitle: 'Direction', + suggestions: [ + { + Name: 'Inbound', + Value: 'inbound', + Description: '', + count: 0, + }, + { + Name: 'Outbound', + Value: 'outbound', + Description: '', + count: 0, + } + ] + }, + exit_node: {}, + asn: {}, + active: { + visible: 'combinedMenu', + menuTitle: 'Active', + suggestions: booleanSuggestionValues(), + }, + tunneled: { + visible: 'combinedMenu', + menuTitle: 'SPN', + suggestions: booleanSuggestionValues(), + tipupKey: 'spn' + }, + from: { + virtual: true + }, + to: { + virtual: true, + }, + }) + + /** Translations for the connection field names */ + keyTranslation = connectionFieldTranslation; + + /** A list of keys for group-by */ + groupByKeys: string[] = []; + + /** A list of keys for sorting */ + orderByKeys: string[] = []; + + ngOnInit(): void { + // Prepare the datasource that is used to initialize the DynamicItemPaginator. + // It basically has a "view" function that executes the current page query + // but with page-number and page-size applied. + // This is used by the paginator to support lazy-loading the different + // result pages. + const dataSource: Datasource = { + view: (page: number, pageSize: number) => { + const query = this.getQuery(); + query.page = page - 1; // UI starts at page 1 while the backend is 0-based + query.pageSize = pageSize; + + return this.netquery.query(query, 'netquery-viewer') + .pipe( + this.helper.attachProfile(), + this.helper.attachPins(), + map(results => { + return (results || []).map(r => { + const grpFilter: Condition = { + ...query.query, + }; + this.groupByKeys.forEach(key => { + grpFilter[key] = r[key]; + }) + + let page = { + ...r, + _chart: !!this.groupByKeys.length ? this.getGroupChart(grpFilter) : null, + _group: !!this.groupByKeys.length ? this.lazyLoadGroup(grpFilter) : null, + } + + return page; + }); + }) + ); + } + } + + // create a new paginator that will use the datasource from above. + this.paginator = new DynamicItemsPaginator(dataSource) + + // subscribe to the search observable that emits a value each time we want to perform + // a new query. + // The actual searching is debounced by second so we don't flood the Portmaster service + // with queries while the user is still configuring their filters. + this.search$ + .pipe( + debounceTime(1000), + switchMap(() => { + this.loading = true; + this.connectionChartData = []; + this.bwChartData = []; + + this.cdr.detectChanges(); + + const query = this.getQuery(); + + // we only load the overall connection chart, the total connection count for the filter result + // as well the the total connection count without any filters here. The actual results are + // loaded by the DynamicItemsPaginator using the "view" function defined above. + return forkJoin({ + query: of(query), + response: this.netquery.batch({ + totalCount: { + ...query, + select: { $count: { field: '*', as: 'totalCount' } }, + }, + + totalConnCount: { + ...query, + select: { + $count: { field: '*', as: 'totalConnCount' } + }, + } + }) + .pipe( + map(response => { + // the the correct resulsts here which depend on whether or not + // we're applying a group by. + let totalCount = 0; + if (this.groupByKeys.length === 0) { + totalCount = response.totalCount[0].totalCount; + } else { + totalCount = response.totalCount.length; + } + + return { + totalCount, + totalConnCount: response.totalConnCount, + } + }) + ), + }) + }), + ) + .subscribe(result => { + this.paginator.pageLoading$ + .pipe( + skip(1), + takeUntil(this.search$), // skip loading the chart if the user trigger a subsequent search + filter(loading => !loading), + take(1), + switchMap(() => forkJoin({ + connectionChart: this.netquery.activeConnectionChart(result.query.query!) + .pipe( + catchError(err => { + this.actionIndicator.error( + 'Internal Error', + 'Failed to load chart: ' + this.actionIndicator.getErrorMessgae(err) + ); + + return of([] as ChartResult[]); + }), + ), + bwChart: this.netquery.bandwidthChart(result.query.query!, [], 60) + })), + ) + .subscribe(chart => { + this.connectionChartData = chart.connectionChart; + this.bwChartData = chart.bwChart; + + this.cdr.markForCheck(); + }) + + // reset the paginator with the new total result count and + // open the first page. + this.paginator.reset(result.response.totalCount); + this.totalConnCount = result.response.totalConnCount[0].totalConnCount; + this.totalResultCount = result.response.totalCount; + + // update the current URL to include the new search + // query and make sure we skip the parameter-update emitted by + // router. + if (!this.skipUrlUpdate) { + this.skipNextRouteUpdate = true; + + const queryText = this.getQueryString(); + + this.filterChange.next(queryText); + + // note that since we only update the query parameters and stay on + // the current route this component will not get re-created but will + // rather receive an update on the queryParamMap (see below). + this.router.navigate([], { + relativeTo: this.route, + queryParams: { + ...this.route.snapshot.queryParams, + q: queryText, + }, + }) + } + this.skipUrlUpdate = false; + + this.loading = false; + this.cdr.markForCheck(); + }) + + // subscribe to router updates so we apply the filter that is part of + // the current query parameter "q". + // We might ignore updates here depending on the value of "skipNextRouterUpdate". + // This is required as we keep the route parameters in sync with the current filter. + this.route.queryParamMap + .pipe( + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(params => { + if (this.skipNextRouteUpdate) { + this.skipNextRouteUpdate = false; + return; + } + + const query = params.get("q") + + if (query !== null) { + objKeys(this.models).forEach(key => { + this.models[key]!.searchValues = []; + }) + + const result = Parser.parse(query!) + + this.onFieldsParsed({ + ...result.conditions, + groupBy: result.groupBy, + orderBy: result.orderBy, + }); + this.textSearch = result.textQuery; + } + + this.skipUrlUpdate = true; + this.performSearch(); + }) + + // we might get new search values from our helper service + // in case the user "SHIFT-Clicks" a SfngAddToFilter directive. + this.helper.onFieldsAdded() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(fields => this.onFieldsParsed(fields)) + + // updateTagBar$ always emits a value when we need to update the current tag-bar values. + // This must always be done if the current search filter has been modified in either of + // the supported ways. + this.updateTagBar$ + .pipe( + takeUntilDestroyed(this.destroyRef), + switchMap(() => { + const obs: Observable<{ [key: string]: (PossilbeValue & QueryResult)[] }>[] = []; + + // for the tag bar we try to show some pretty names for values that are meant to be + // internal (like the number-constants for the verdicts or using the profile name instead + // of the profile ID). Since we might need to load data from the Portmaster for this (like + // for profile names) we construct a list of observables using helper.encodeToPossibleValues + // and use the result for the tagbar. + Object.keys(this.models) + .sort() // make sure we always output values in a constant order + .forEach(modelKey => { + const values = this.models[modelKey]!.searchValues; + + if (values.length > 0) { + obs.push( + of(values.map(val => ({ + [modelKey]: val, + }))) + .pipe( + this.helper.encodeToPossibleValues(modelKey), + map(result => ({ + [modelKey]: result, + })) + ) + ) + } + }) + + if (obs.length === 0) { + return of([]); + } + + return combineLatest(obs); + }) + ) + .subscribe(tagBarValues => { + this.tagbarValues = []; + + // reset the "selected" field of each model that is shown in the "combinedMenu". + // we'll set the correct ones as "selected" again in the next step. + objKeys(this.models).forEach(key => { + if (this.models[key]?.visible === 'combinedMenu') { + this.models[key]?.suggestions.forEach(val => val.selected = false); + } + }) + + // finally construct a new list of tag-bar values and update the "selected" field of + // suggested-values for the "combinedMenu" items based on the actual search values. + tagBarValues.forEach(obj => { + objKeys(obj).forEach(key => { + if (obj[key].length > 0) { + this.tagbarValues.push({ + key: key as string, + values: obj[key], + }) + + // update the `selected` field of suggested-values for each model that is displayed in the combined-menu + const modelsKey = key as keyof NetqueryConnection; + if (this.models[modelsKey]?.visible === 'combinedMenu') + this.models[modelsKey]?.suggestions.forEach(suggestedValue => { + suggestedValue.selected = obj[key].some(val => val.Value === suggestedValue.Value); + }) + } + }) + }) + + this.cdr.markForCheck(); + }) + + // handle any filter preset + // + if (!!this.filterPreset) { + try { + const result = Parser.parse(this.filterPreset); + this.onFieldsParsed({ + ...result.conditions, + groupBy: result.groupBy, + orderBy: result.orderBy, + }); + } catch (err) { + // only log the error in dev mode as this is most likely + // just bad user input + if (isDevMode()) { + console.error(err); + } + } + } + } + + ngAfterViewInit(): void { + // once we are initialized decide which pro-tip we want to show this time... + this.proTipIdx = Math.floor(Math.random() * this.proTips.length); + } + + ngOnDestroy() { + this.paginator.clear(); + this.search$.complete(); + this.helper.dispose(); + } + + // lazyLoadGroup returns an observable that will emit a DynamicItemsPaginator once subscribed. + // This is used in "group-by" views to lazy-load the content of the group once the user + // expands it. + lazyLoadGroup(groupFilter: Condition): Observable> { + return new Observable(observer => { + this.netquery.query({ + query: groupFilter, + select: [ + { $count: { field: "*", as: "totalCount" } } + ], + orderBy: [ + { field: 'started', desc: true }, + { field: 'ended', desc: true } + ], + databases: this.databases, + }, 'netquery-viewer-load-group') + .subscribe(result => { + const paginator = new DynamicItemsPaginator({ + view: (pageNumber: number, pageSize: number) => { + return this.netquery.query({ + query: groupFilter, + orderBy: [ + { field: 'started', desc: true }, + { field: 'ended', desc: true } + ], + page: pageNumber - 1, + pageSize: pageSize, + databases: this.databases, + }, 'netquery-viewer-group-paginator') as Observable; + } + }, 25) + + paginator.reset(result[0]?.totalCount || 0) + + observer.next(paginator) + }) + }) + } + + // Returns an observable that loads the current active connection chart using the + // current page query but only for the condition of the displayed group. + getGroupChart(groupFilter: Condition): Observable { + return this.netquery.activeConnectionChart(groupFilter) + } + + // loadSuggestion loads possible values for a given connection field + // and updates the "suggestions" field of the correct models entry. + // It also uses helper.encodeToPossibleValues to make sure we show + // pretty names for otherwise "internal" values like verdict constants + // or profile IDs. + loadSuggestion(field: string): void; + loadSuggestion(field: T) { + const search = this.getQuery([field]); + + this.models[field]!.loading = !this.models[field]!.suggestions?.length; + + this.netquery.query({ + select: [ + field, + { + $count: { + field: "*", + as: "count" + }, + } + ], + query: search.query, + groupBy: [ + field, + ], + orderBy: [{ field: "count", desc: true }, { field, desc: true }], + databases: this.databases, + }, 'netquery-viewer-load-suggestions') + .pipe(this.helper.encodeToPossibleValues(field)) + .subscribe(result => { + this.models[field]!.loading = false; + + // create a set that we can use to lookup if a value + // is currently selected. + // This is needed to ensure selected values are sorted to the top. + let currentlySelected = new Set(); + this.models[field]!.searchValues.forEach( + val => currentlySelected.add(val) + ); + + this.models[field]!.suggestions = + result + .sort((a, b) => { + const hasA = currentlySelected.has(a.Value); + const hasB = currentlySelected.has(b.Value); + + if (hasA && !hasB) { + return -1; + } + if (hasB && !hasA) { + return 1; + } + + return b.count - a.count; + }) as any; + + this.cdr.markForCheck(); + }) + } + + sortByCount(a: SelectOption, b: SelectOption) { + return b.data - a.data + } + + /** @private Callback for keyboard events on the search-input */ + onFieldsParsed(fields: SfngSearchbarFields, replace = false) { + const allowedKeys = new Set(Object.keys(this.models)) + + objKeys(fields).forEach(key => { + if (key === 'groupBy') { + this.groupByKeys = (fields.groupBy || this.groupByKeys) + .filter(val => { + // an empty value is just filtered out without an error as this is the only + // way to specify "I don't want grouping" via the filter + if (val === '') { + return false; + } + + if (!allowedKeys.has(val as any)) { + this.actionIndicator.error("Invalid search query", "Column " + val + " is not allowed for groupby") + return false; + } + return true; + }) + + return; + } + + if (key === 'orderBy') { + this.orderByKeys = (fields.orderBy || this.orderByKeys) + .filter(val => { + if (!allowedKeys.has(val as any)) { + this.actionIndicator.error("Invalid search query", "Column " + val + " is not allowed for orderby") + return false; + } + return true; + }) + + return; + } + + if (!allowedKeys.has(key)) { + this.actionIndicator.error("Invalid search query", "Column " + key + " is not allowed for filtering"); + return; + } + + if (fields[key]?.length === 0 && replace) { + this.models[key].searchValues = []; + } else { + fields[key]!.forEach(val => { + // quick fix to make sure domains always end in a period. + if (key === 'domain' && typeof val === 'string' && val.length > 0 && !val.endsWith('.')) { + val = `${val}.` + } + + if (typeof val === 'object' && '$ne' in val) { + this.actionIndicator.error("NOT conditions are not yet supported") + return; + } + + // avoid duplicates + if (this.models[key]!.searchValues.includes(val)) { + return; + } + + if (!replace) { + this.models[key]!.searchValues = [ + ...this.models[key]!.searchValues, + val, + ] + } else { + this.models[key]!.searchValues = [val] + } + }) + } + + this.updateDateRangeState() + }) + + this.cdr.markForCheck(); + + this.performSearch(); + } + + /** @private Query the portmaster service for connections matching the current settings */ + performSearch() { + this.loading = true; + this.lastReload = new Date(); + this.paginator.clear() + this.search$.next(); + this.updateTagbarValues(); + } + + /** @private Returns the current query in it's string representation */ + getQueryString(): string { + let result = ''; + + objKeys(this.models).forEach(key => { + this.models[key]?.searchValues.forEach(val => { + // we use JSON.stringify here to make sure the value is + // correclty quoted. + result += `${key}:${JSON.stringify(val)} `; + }) + }) + + if (result.length > 0 && this.textSearch.length > 0) { + result += ' ' + } + + this.groupByKeys.forEach(key => { + result += `groupby:"${key}" ` + }) + this.orderByKeys.forEach(key => { + result += `orderby:"${key}" ` + }) + + if (result.length > 0 && this.textSearch.length > 0) { + result += ' ' + } + + result += `${this.textSearch}` + + return result; + } + + /** @private Copies the current query into the user clipboard */ + copyQuery() { + this.integration.writeToClipboard(this.getQueryString()) + .then(() => { + this.actionIndicator.success("Query copied to clipboard", 'Go ahead and share your query!') + }) + .catch((err) => { + this.actionIndicator.error('Failed to copy to clipboard', this.actionIndicator.getErrorMessgae(err)) + }) + } + + /** @private Clears the current query */ + clearQuery() { + objKeys(this.models).forEach(key => { + this.models[key]!.searchValues = []; + }) + this.textSearch = ''; + + this.updateTagbarValues(); + this.performSearch(); + } + + /** @private Constructs a query from the current page settings. Supports excluding certain fields from the query. */ + getQuery(excludeFields: string[] = []): Query { + let query: Condition = {} + let textSearch: Query['textSearch']; + + const dateQuery = this.getDateRangeCondition() + if (dateQuery !== null) { + query = mergeConditions(query, dateQuery) + } + + // create the query conditions for all keys on this.models + Object.keys(this.models).forEach((key: string) => { + if (excludeFields.includes(key)) { + return; + } + + if (this.models[key]!.searchValues.length > 0) { + // check if model is virtual, and if, skip adding it to the query + if (this.models[key].virtual) { + return + } + + query[key] = { + $in: this.models[key]!.searchValues, + } + } + }) + + if (this.expertise.currentLevel !== 'developer') { + query["internal"] = { + $eq: false, + } + } + + if (this.textSearch !== '') { + textSearch = { + fields: freeTextSearchFields, + value: this.textSearch + } + } + + let select: Query['select'] | undefined = undefined; + if (!!this.groupByKeys.length) { + // we always want to show the total and the number of allowed connections + // per group so we need to add those to the select part of the query + select = [ + { + $count: { + field: "*", + as: "totalCount", + }, + }, + { + $sum: { + condition: { + verdict: { + $in: [ + Verdict.Accept, + Verdict.RerouteToNs, + Verdict.RerouteToTunnel + ], + } + }, + as: "countAllowed" + } + }, + ...this.groupByKeys, + ] + } + + let normalizedQuery = mergeConditions(query, this.mergeFilter || {}) + + let orderBy: string[] | OrderBy[] = this.orderByKeys; + if (!orderBy || orderBy.length === 0) { + orderBy = [ + { + field: 'started', + desc: true, + }, + { + field: 'ended', + desc: true, + } + ] + } + + return { + select: select, + query: normalizedQuery, + groupBy: this.groupByKeys, + orderBy: orderBy, + textSearch, + databases: this.databases, + } + } + + /** @private Updates the current model form all values emited by the tag-bar. */ + onTagbarChange(tagKinds: SfngTagbarValue[]) { + objKeys(this.models).forEach(key => { + this.models[key]!.searchValues = []; + }); + + tagKinds.forEach(kind => { + const key = kind.key as keyof NetqueryConnection; + this.models[key]!.searchValues = kind.values.map(possibleValue => possibleValue.Value); + + if (this.models[key]?.visible === 'combinedMenu') + this.models[key]?.suggestions.forEach(val => { + val.selected = this.models[key]!.searchValues.find(searchValue => searchValue === val.Value) + }) + }) + + this.updateDateRangeState(); + + this.performSearch(); + } + + onDateRangeChange(event: Date[]) { + if (event.length >= 1) { + event[0] = new Date(event[0].getFullYear(), event[0].getMonth(), event[0].getDate(), 0, 0, 0) + this.onFieldsParsed({ from: [formatDate(event[0], 'medium', this.localeId)] }, true) + } else { + this.onFieldsParsed({ from: [] }, true) + } + + if (event.length >= 2) { + event[1] = new Date(event[1].getFullYear(), event[1].getMonth(), event[1].getDate() + 1, 0, 0, -1) + this.onFieldsParsed({ to: [formatDate(event[1], 'medium', this.localeId)] }, true) + } else { + this.onFieldsParsed({ to: [] }, true) + } + } + + /** Updates the {@link tagbarValues} from {@link models}*/ + private updateTagbarValues() { + this.updateTagBar$.next(); + } + + private isValidFilter(key: string): key is keyof NetqueryConnection { + return Object.keys(this.models).includes(key); + } + + useAsFilter(rec: QueryResult) { + const keys = new Set(objKeys(this.models)) + + // reset the search values + keys.forEach(key => { + this.models[key]!.searchValues = []; + }) + + objKeys(rec).forEach(key => { + if (keys.has(key as keyof NetqueryConnection)) { + this.models[key as keyof NetqueryConnection]!.searchValues = [rec[key]]; + } + }) + + // reset the group-by-keys since they don't make any sense anymore. + this.groupByKeys = []; + this.performSearch(); + } + + /** @private - used by the combined filter menu */ + toggleCombinedMenuFilter(key: string, value: Suggestion) { + const k = key as keyof NetqueryConnection; + if (value.selected) { + this.models[k]!.searchValues = this.models[k]?.searchValues.filter(val => val !== value.Value) || []; + } else { + this.models[k]!.searchValues.push(value.Value) + } + + this.updateTagbarValues(); + this.performSearch(); + } + + trackSuggestion: TrackByFunction = (_: number, s: Suggestion) => s.Name + '::' + s.Value; +} + +function initializeModels(models: { [key: string]: Partial> }): { [key: string]: Model } { + objKeys(models).forEach(key => { + models[key] = { + suggestions: [], + searchValues: [], + visible: false, + loading: false, + ...models[key], + } + }) + + return models as any; +} + +function booleanSuggestionValues(): Suggestion[] { + return [ + { + Name: 'Yes', + Value: true, + Description: '', + count: 0, + }, + { + Name: 'No', + Value: false, + Description: '', + count: 0, + }, + ] +} diff --git a/desktop/angular/src/app/shared/netquery/netquery.module.ts b/desktop/angular/src/app/shared/netquery/netquery.module.ts new file mode 100644 index 00000000..5a433666 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/netquery.module.ts @@ -0,0 +1,88 @@ +import { A11yModule } from "@angular/cdk/a11y"; +import { OverlayModule } from "@angular/cdk/overlay"; +import { CommonModule } from "@angular/common"; +import { inject, NgModule } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; +import { SfngAccordionModule, SfngDropDownModule, SfngPaginationModule, SfngSelectModule, SfngTipUpModule, SfngToggleSwitchModule, SfngTooltipModule } from "@safing/ui"; +import { NzDatePickerModule } from 'ng-zorro-antd/date-picker'; +import { SfngAppIconModule } from "../app-icon"; +import { CountIndicatorModule } from "../count-indicator"; +import { CountryFlagModule } from "../country-flag"; +import { ExpertiseModule } from "../expertise/expertise.module"; +import { SfngFocusModule } from "../focus"; +import { SfngMenuModule } from "../menu"; +import { CommonPipesModule } from "../pipes"; +import { SPNModule } from './../../pages/spn/spn.module'; +import { SfngNetqueryAddToFilterDirective } from "./add-to-filter"; +import { CombinedMenuPipe } from "./combined-menu.pipe"; +import { SfngNetqueryConnectionDetailsComponent } from "./connection-details"; +import { SfngNetqueryConnectionRowComponent } from "./connection-row"; +import { SfngNetqueryLineChartComponent } from "./line-chart/line-chart"; +import { SfngNetqueryViewer } from "./netquery.component"; +import { CanShowConnection, CanUseRulesPipe, ConnectionLocationPipe, CountryNamePipe, CountryNameService, IsBlockedConnectionPipe } from "./pipes"; +import { SfngNetqueryScopeLabelComponent } from "./scope-label"; +import { SfngNetquerySearchOverlayComponent } from "./search-overlay"; +import { SfngNetquerySearchbarComponent, SfngNetquerySuggestionDirective } from "./searchbar"; +import { SfngNetqueryTagbarComponent } from "./tag-bar"; +import { CircularBarChartComponent } from './circular-bar-chart/circular-bar-chart.component'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + CountryFlagModule, + SfngDropDownModule, + SfngSelectModule, + SfngTooltipModule, + SfngAccordionModule, + SfngMenuModule, + SfngPaginationModule, + SfngFocusModule, + SfngAppIconModule, + SfngTipUpModule, + SfngToggleSwitchModule, + A11yModule, + ExpertiseModule, + OverlayModule, + CountIndicatorModule, + FontAwesomeModule, + CommonPipesModule, + SPNModule, + NzDatePickerModule, + ], + exports: [ + SfngNetqueryViewer, + SfngNetqueryLineChartComponent, + SfngNetquerySearchOverlayComponent, + SfngNetqueryScopeLabelComponent, + CircularBarChartComponent, + ], + declarations: [ + SfngNetqueryViewer, + SfngNetqueryConnectionRowComponent, + SfngNetqueryLineChartComponent, + SfngNetqueryTagbarComponent, + SfngNetquerySearchbarComponent, + SfngNetquerySearchOverlayComponent, + SfngNetquerySuggestionDirective, + SfngNetqueryScopeLabelComponent, + SfngNetqueryConnectionDetailsComponent, + SfngNetqueryAddToFilterDirective, + ConnectionLocationPipe, + IsBlockedConnectionPipe, + CanUseRulesPipe, + CanShowConnection, + CombinedMenuPipe, + CircularBarChartComponent, + CountryNamePipe, + ], + providers: [ + CountryNameService + ] +}) +export class NetqueryModule { + private _unusedBootstrap = [ + inject(CountryNameService), // make sure country names are loaded on bootstrap + ] +} diff --git a/desktop/angular/src/app/shared/netquery/pipes/can-show.pipe.ts b/desktop/angular/src/app/shared/netquery/pipes/can-show.pipe.ts new file mode 100644 index 00000000..35f93628 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/pipes/can-show.pipe.ts @@ -0,0 +1,22 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { ExpertiseLevel, NetqueryConnection } from "@safing/portmaster-api"; + + +@Pipe({ + name: "canShowConnection", + pure: true, +}) +export class CanShowConnection implements PipeTransform { + transform(conn: NetqueryConnection, level: ExpertiseLevel) { + if (!conn) { + return false; + } + if (level === ExpertiseLevel.Developer) { + // we show all connections for developers + return true; + } + // if we are in advanced or simple mode we should + // hide internal connections. + return !conn.internal; + } +} diff --git a/desktop/angular/src/app/shared/netquery/pipes/can-use-rules.pipe.ts b/desktop/angular/src/app/shared/netquery/pipes/can-use-rules.pipe.ts new file mode 100644 index 00000000..d4b5d4d3 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/pipes/can-use-rules.pipe.ts @@ -0,0 +1,32 @@ + +// the following settings are stronger than rules +// and cannot be "fixed" by creating a new allow/deny + +import { Pipe, PipeTransform } from "@angular/core"; +import { IsDenied, NetqueryConnection } from "@safing/portmaster-api"; + +// rule. +let optionKeys = new Set([ + "filter/blockInternet", + "filter/blockLAN", + "filter/blockLocal", + "filter/blockP2P", + "filter/blockInbound" +]) + +@Pipe({ + name: "canUseRules", + pure: true, +}) +export class CanUseRulesPipe implements PipeTransform { + transform(conn: NetqueryConnection): boolean { + if (!conn) { + return false; + } + if (!!conn.extra_data?.reason?.OptionKey && IsDenied(conn.verdict)) { + return !optionKeys.has(conn.extra_data.reason.OptionKey); + } + return true; + } +} + diff --git a/desktop/angular/src/app/shared/netquery/pipes/country-name.pipe.ts b/desktop/angular/src/app/shared/netquery/pipes/country-name.pipe.ts new file mode 100644 index 00000000..93e6bc61 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/pipes/country-name.pipe.ts @@ -0,0 +1,59 @@ +import { HttpClient } from '@angular/common/http'; +import { Pipe, PipeTransform, Injectable, inject } from '@angular/core'; +import { GeoCoordinates, SPNService } from '@safing/portmaster-api'; +import { environment } from 'src/environments/environment'; +import { ActionIndicatorService } from '../../action-indicator'; +import { objKeys } from '../../utils'; + +export interface CountryListResponse { + [countryKey: string]: { + Code: string; + Name: string; + Center: GeoCoordinates; + Continent: { + Code: string; + Region: string; + Name: string; + } + } +} + +@Injectable() +export class CountryNameService { + private readonly spn = inject(SPNService); + private readonly http = inject(HttpClient); + private readonly uai = inject(ActionIndicatorService); + + private map: Map = new Map(); + + constructor() { + this.http.get(`${environment.httpAPI}/v1/intel/geoip/countries`) + .subscribe({ + next: response => { + objKeys(response) + .forEach(key => { + this.map.set(key as string, response[key].Name); + }); + }, + error: err => { + this.uai.error('Failed to fetch country data', this.uai.getErrorMessage(err)); + } + }) + } + + resolveName(code: string): string { + return this.map.get(code) || ''; + } +} + +@Pipe({ + name: 'countryName', + pure: true, +}) +export class CountryNamePipe implements PipeTransform { + private countryService = inject(CountryNameService); + + transform(countryCode: string) { + return this.countryService.resolveName(countryCode); + } +} diff --git a/desktop/angular/src/app/shared/netquery/pipes/index.ts b/desktop/angular/src/app/shared/netquery/pipes/index.ts new file mode 100644 index 00000000..9b429e59 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/pipes/index.ts @@ -0,0 +1,5 @@ +export * from './location.pipe'; +export * from './can-show.pipe'; +export * from './can-use-rules.pipe'; +export * from './is-blocked.pipe'; +export * from './country-name.pipe'; diff --git a/desktop/angular/src/app/shared/netquery/pipes/is-blocked.pipe.ts b/desktop/angular/src/app/shared/netquery/pipes/is-blocked.pipe.ts new file mode 100644 index 00000000..fcb6dc0d --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/pipes/is-blocked.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { IsDenied, NetqueryConnection } from '@safing/portmaster-api'; + +@Pipe({ + name: "isBlocked", + pure: true +}) +export class IsBlockedConnectionPipe implements PipeTransform { + transform(conn: NetqueryConnection): boolean { + return IsDenied(conn?.verdict); + } +} diff --git a/desktop/angular/src/app/shared/netquery/pipes/location.pipe.ts b/desktop/angular/src/app/shared/netquery/pipes/location.pipe.ts new file mode 100644 index 00000000..522ed86a --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/pipes/location.pipe.ts @@ -0,0 +1,33 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { IsGlobalScope, IsLANScope, IsLocalhost, NetqueryConnection } from '@safing/portmaster-api'; + +@Pipe({ + name: 'connectionLocation', + pure: true, +}) +export class ConnectionLocationPipe implements PipeTransform { + transform(conn: NetqueryConnection): string { + if (conn.type === 'dns') { + return ''; + } + if (!!conn.country) { + return conn.country; + } + + const scope = conn.scope; + + if (IsGlobalScope(scope)) { + return 'Internet' + } + + if (IsLANScope(scope)) { + return 'LAN'; + } + + if (IsLocalhost(scope)) { + return 'Device' + } + + return ''; + } +} diff --git a/desktop/angular/src/app/shared/netquery/scope-label/index.ts b/desktop/angular/src/app/shared/netquery/scope-label/index.ts new file mode 100644 index 00000000..8f481940 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/scope-label/index.ts @@ -0,0 +1 @@ +export * from './scope-label'; diff --git a/desktop/angular/src/app/shared/netquery/scope-label/scope-label.html b/desktop/angular/src/app/shared/netquery/scope-label/scope-label.html new file mode 100644 index 00000000..0df11b57 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/scope-label/scope-label.html @@ -0,0 +1,8 @@ + + {{subdomain}}. + {{domain}} + + + {{ scopeTranslation[scope || ''] || 'N/A' }} + diff --git a/desktop/angular/src/app/shared/netquery/scope-label/scope-label.ts b/desktop/angular/src/app/shared/netquery/scope-label/scope-label.ts new file mode 100644 index 00000000..8bb64c83 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/scope-label/scope-label.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { ScopeTranslation } from '@safing/portmaster-api'; +import { parseDomain } from '../../utils'; + +@Component({ + selector: 'sfng-netquery-scope-label', + templateUrl: 'scope-label.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SfngNetqueryScopeLabelComponent implements OnChanges { + readonly scopeTranslation = ScopeTranslation; + + @Input() + scope?: string = '' + + @Input() + set leftRightFix(v: any) { + console.warn("deprecated @Input usage") + } + get leftRightFix() { return false } + + domain: string = ''; + subdomain: string = ''; + + ngOnChanges(change: SimpleChanges) { + if (!!change['scope']) { + //this.label = change.label.currentValue; + const result = parseDomain(change.scope.currentValue || '') + + this.domain = result?.domain || ''; + this.subdomain = result?.subdomain || ''; + } + } +} diff --git a/desktop/angular/src/app/shared/netquery/search-overlay/index.ts b/desktop/angular/src/app/shared/netquery/search-overlay/index.ts new file mode 100644 index 00000000..ffad6a32 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/search-overlay/index.ts @@ -0,0 +1 @@ +export * from './search-overlay'; diff --git a/desktop/angular/src/app/shared/netquery/search-overlay/search-overlay.html b/desktop/angular/src/app/shared/netquery/search-overlay/search-overlay.html new file mode 100644 index 00000000..49eaa84b --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/search-overlay/search-overlay.html @@ -0,0 +1,2 @@ + diff --git a/desktop/angular/src/app/shared/netquery/search-overlay/search-overlay.ts b/desktop/angular/src/app/shared/netquery/search-overlay/search-overlay.ts new file mode 100644 index 00000000..eaae1a08 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/search-overlay/search-overlay.ts @@ -0,0 +1,81 @@ +import { ChangeDetectionStrategy, Component, Inject } from "@angular/core"; +import { Router } from "@angular/router"; +import { SfngDialogRef, SFNG_DIALOG_REF } from "@safing/ui"; +import { objKeys } from "../../utils"; +import { NetqueryHelper } from "../connection-helper.service"; +import { SfngSearchbarFields } from "../searchbar"; +import { connectionFieldTranslation } from "../utils"; + +@Component({ + selector: 'sfng-netquery-search-overlay', + templateUrl: './search-overlay.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + NetqueryHelper, + ], + styles: [ + ` + :host { + @apply block; + width: 700px; + } + + ::ng-deep sfng-netquery-search-overlay sfng-netquery-searchbar input { + border: 1px solid theme("colors.gray.200") !important; + } + ` + ] +}) +export class SfngNetquerySearchOverlayComponent { + keyTranslation = connectionFieldTranslation; + + textSearch = ''; + + fields: SfngSearchbarFields = {}; + + constructor( + @Inject(SFNG_DIALOG_REF) private dialogRef: SfngDialogRef, + private router: Router, + ) { } + + performSearch() { + let query = ""; + const fields = objKeys(this.fields) + + // if there's only one profile key directly navigate the user to the app view + if (fields.length === 1 && fields[0] === 'profile' && this.fields.profile!.length === 1) { + let profileName: string = this.fields.profile![0] || ''; + if (!profileName.includes("/")) { + profileName = "local/" + profileName + } + this.router.navigate(['/app/' + profileName || '']) + this.dialogRef.close(); + return; + } + + fields.forEach(field => { + this.fields[field]?.forEach(value => { + query += `${field}:${JSON.stringify(value)} ` + }) + }) + + if (query !== '' && this.textSearch !== '') { + query += " " + } + query += this.textSearch; + + this.router.navigate(['/monitor'], { + queryParams: { + q: query, + } + }) + + this.dialogRef.close(); + } + + onFieldsParsed(fields: SfngSearchbarFields) { + objKeys(fields).forEach(field => { + this.fields[field] = [...(this.fields[field] || []), ...(fields[field] || [])]; + }) + } +} diff --git a/desktop/angular/src/app/shared/netquery/searchbar/index.ts b/desktop/angular/src/app/shared/netquery/searchbar/index.ts new file mode 100644 index 00000000..2520d4d6 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/searchbar/index.ts @@ -0,0 +1 @@ +export * from './searchbar'; diff --git a/desktop/angular/src/app/shared/netquery/searchbar/searchbar.html b/desktop/angular/src/app/shared/netquery/searchbar/searchbar.html new file mode 100644 index 00000000..ef2dedf4 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/searchbar/searchbar.html @@ -0,0 +1,76 @@ +
+
+ + + +
+ +
+ + + +
+ +
+
+ + +
    +
  • + Full-Text Search: {{ textSearch }} +
  • +
+ +
+ + +
+

+ Filter by {{ labels[sug.field] || sug.field }} +

+
    +
  • + {{ val.display || (val.value === '' ? 'N/A' : val.value) }} + #{{ val.count }} connections +
  • +
+
+
+ +
+ + + + + + Loading suggestions ... +
+ + + + There are no suggestions for your query. Press +
Enter
+ to + perform a full text search. +
+
+
+
diff --git a/desktop/angular/src/app/shared/netquery/searchbar/searchbar.ts b/desktop/angular/src/app/shared/netquery/searchbar/searchbar.ts new file mode 100644 index 00000000..46a42f51 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/searchbar/searchbar.ts @@ -0,0 +1,437 @@ +import { ListKeyManager } from "@angular/cdk/a11y"; +import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { CdkOverlayOrigin } from "@angular/cdk/overlay"; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, Directive, ElementRef, EventEmitter, forwardRef, HostBinding, HostListener, inject, Input, OnDestroy, OnInit, Output, QueryList, TrackByFunction, ViewChild, ViewChildren } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { Condition, ExpertiseLevel, Netquery, NetqueryConnection } from "@safing/portmaster-api"; +import { SfngDropdownComponent } from "@safing/ui"; +import { combineLatest, Observable, of, Subject } from "rxjs"; +import { catchError, debounceTime, map, switchMap } from "rxjs/operators"; +import { fadeInAnimation, fadeInListAnimation } from "../../animations"; +import { ExpertiseService } from "../../expertise"; +import { objKeys } from "../../utils"; +import { NetqueryHelper } from "../connection-helper.service"; +import { Parser, ParseResult } from "../textql"; + +export type SfngSearchbarFields = { + [key in keyof Partial]: any[]; +} & { + groupBy?: string[]; + orderBy?: string[]; + from?: string[]; + to?: string[]; +} + +export type SfngSearchbarSuggestionValue = { + value: NetqueryConnection[K]; + display: string; + count: number; +} + +export type SfngSearchbarSuggestion = { + start?: number; + field: K | '_textsearch'; + values: SfngSearchbarSuggestionValue[]; +} + +@Directive({ + selector: '[sfngNetquerySuggestion]', + exportAs: 'sfngNetquerySuggestion' +}) +export class SfngNetquerySuggestionDirective { + constructor() { } + + @Input() + sfngSuggestion?: SfngSearchbarSuggestion; + + @Input() + sfngNetquerySuggestion?: SfngSearchbarSuggestionValue | string; + + set active(v: any) { + this._active = coerceBooleanProperty(v); + } + get active() { + return this._active; + } + private _active: boolean = false; + + getLabel(): string { + if (typeof this.sfngNetquerySuggestion === 'string') { + return this.sfngNetquerySuggestion; + } + return '' + this.sfngNetquerySuggestion?.value; + } +} + +@Component({ + selector: 'sfng-netquery-searchbar', + templateUrl: './searchbar.html', + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + fadeInAnimation, + fadeInListAnimation + ], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SfngNetquerySearchbarComponent), + multi: true, + } + ] +}) +export class SfngNetquerySearchbarComponent implements ControlValueAccessor, OnInit, OnDestroy, AfterViewInit { + private loadSuggestions$ = new Subject(); + private triggerDropdownClose$ = new Subject(); + private keyManager!: ListKeyManager>; + private destroyRef = inject(DestroyRef); + + /** Whether or not we are currently loading suggestions */ + loading = false; + + @ViewChild(CdkOverlayOrigin, { static: true }) + searchBoxOverlayOrigin!: CdkOverlayOrigin; + + @ViewChild(SfngDropdownComponent) + suggestionDropDown?: SfngDropdownComponent; + + @ViewChild('searchBar', { static: true, read: ElementRef }) + searchBar!: ElementRef; + + @ViewChildren(SfngNetquerySuggestionDirective) + suggestionValues!: QueryList>; + + @Output() + fieldsParsed = new EventEmitter(); + + @Input() + labels: { [key: string]: string } = {} + + /** Controls whether or not suggestions are shown as a drop-down or inline */ + @Input() + mode: 'inline' | 'default' = 'default'; + + suggestions: SfngSearchbarSuggestion[] = []; + + textSearch = ''; + + @HostListener('focus') + onFocus() { + // move focus forward to the input element + this.searchBar.nativeElement.focus(); + } + + @Input() + @HostBinding('tabindex') + tabindex = 0; + + writeValue(val: string): void { + if (typeof val === 'string') { + const result = Parser.parse(val); + this.textSearch = result.textQuery; + } else { + this.textSearch = ''; + } + this.cdr.markForCheck(); + } + + _onChange: (val: string) => void = () => { } + registerOnChange(fn: any): void { + this._onChange = fn; + } + + _onTouched: () => void = () => { } + registerOnTouched(fn: any): void { + this._onTouched = fn + } + + ngAfterViewInit(): void { + this.keyManager = new ListKeyManager(this.suggestionValues) + .withVerticalOrientation() + .withTypeAhead() + .withHomeAndEnd() + .withWrap(); + + this.keyManager.change + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(idx => { + if (!this.suggestionValues.length) { + return + } + + this.suggestionValues.forEach(val => val.active = false); + this.suggestionValues.get(idx)!.active = true; + this.cdr.markForCheck(); + }); + } + + ngOnInit(): void { + this.loadSuggestions$ + .pipe( + debounceTime(500), + switchMap(() => { + let fields: (keyof NetqueryConnection)[] = [ + 'profile', + 'domain', + 'as_owner', + 'remote_ip', + ]; + let limit = 3; + + const parser = new Parser(this.textSearch); + const parseResult = parser.process(); + + const queries: Observable>[] = []; + const queryKeys: (keyof Partial)[] = []; + + // FIXME(ppacher): confirm .type is an actually allowed field + if (!!parser.lastUnterminatedCondition) { + fields = [parser.lastUnterminatedCondition.type as keyof NetqueryConnection]; + limit = 0; + } + + fields.forEach(field => { + let queryField = field; + + // if we are searching the profiles we use the profile name + // rather than the profile_id for searching. + if (field === 'profile') { + queryField = 'profile_name'; + } + + const query: Condition = { + [queryField]: { + $like: `%${!!parser.lastUnterminatedCondition ? parser.lastUnterminatedCondition.value : parseResult.textQuery}%` + }, + } + + // hide internal connections if the user is not a developer + if (this.expertiseService.currentLevel !== ExpertiseLevel.Developer) { + query.internal = { + $eq: false + } + } + + const obs = this.netquery.query({ + select: [ + field, + { + $count: { + field: "*", + as: "count" + }, + } + ], + query: query, + groupBy: [ + field, + ], + page: 0, + pageSize: limit, + orderBy: [{ field: "count", desc: true }] + }, 'netquery-searchbar-get-counts') + .pipe( + this.helper.encodeToPossibleValues(field), + map(results => { + let val: SfngSearchbarSuggestion = { + field: field, + values: [], + start: parser.lastUnterminatedCondition ? parser.lastUnterminatedCondition.start : undefined, + } + + results.forEach(res => { + val.values.push({ + value: res.Value, + display: res.Name, + count: res.count, + }) + }) + + return val; + }), + catchError(err => { + console.error(err); + + return of({ + field: field, + values: [], + }) + }) + ) + + queries.push(obs) + queryKeys.push(field) + }) + + return combineLatest(queries) + }), + ) + .subscribe(result => { + this.loading = false; + + this.suggestions = result + .filter((sug: SfngSearchbarSuggestion) => sug.values?.length > 0) + + this.keyManager.setActiveItem(0); + + this.cdr.markForCheck(); + }) + + this.triggerDropdownClose$ + .pipe(debounceTime(100)) + .subscribe(shouldClose => { + if (shouldClose) { + this.suggestionDropDown?.close(); + } + }) + + if (this.mode === 'inline') { + this.loadSuggestions(); + } + } + + ngOnDestroy(): void { + this.loadSuggestions$.complete(); + this.triggerDropdownClose$.complete(); + } + + cancelDropdownClose() { + this.triggerDropdownClose$.next(false); + } + + onSearchModelChange(value: string) { + if (value.length >= 3 || this.mode === 'inline') { + this.loadSuggestions(); + } else if (this.suggestionDropDown?.isOpen) { + // close the suggestion dropdown if the search input contains less than + // 3 characters and we're currently showing the dropdown + this.suggestionDropDown?.close(); + } + } + + /** @private Callback for keyboard events on the search-input */ + onSearchKeyDown(event: KeyboardEvent) { + if (event.key === ' ' && event.ctrlKey) { + this.loadSuggestions(); + event.preventDefault(); + event.stopPropagation() + return; + } + + if (event.key === 'Enter') { + + const selectedSuggestion = this.suggestionValues.toArray().findIndex(val => val.active); + if (selectedSuggestion > 0) { // we must skip 0 here as well as that's the dummy element + const sug = this.suggestionValues.get(selectedSuggestion); + this.applySuggestion(sug?.sfngSuggestion?.field, sug?.sfngNetquerySuggestion, event, sug?.sfngSuggestion?.start) + + return; + } + + this.suggestionDropDown?.close(); + this.parseAndEmit(); + this.cdr.markForCheck(); + + return; + } + + this.keyManager.onKeydown(event); + } + + onFocusLost(event: FocusEvent) { + this._onTouched(); + } + + private parseAndEmit() { + const result = Parser.parse(this.textSearch); + this.textSearch = result.textQuery; + + const keys = objKeys(result.conditions) + const meta = { + groupBy: result.groupBy || undefined, + orderBy: result.orderBy || undefined, + } + if (keys.length > 0 || meta.groupBy?.length || meta.orderBy?.length) { + let updatedConditions: ParseResult['conditions'] = {}; + keys.forEach(key => { + updatedConditions[key] = this.helper.decodePrettyValues(key as keyof NetqueryConnection, result.conditions[key]) + }) + this.fieldsParsed.next({ ...updatedConditions, ...meta }); + } + + this._onChange(this.textSearch); + } + + applySuggestion(field: keyof NetqueryConnection | '_textsearch', val: any, event: { shiftKey: boolean }, start?: number) { + // this is a full-text search so just emit the value, close the dropdown and we're done + if (field === '_textsearch') { + this._onChange(this.textSearch); + this.suggestionDropDown?.close(); + + return + } + + if (start !== undefined) { + this.textSearch = this.textSearch.slice(0, start) + } else if (!event.shiftKey) { + this.textSearch = ''; + } else { + // the user pressed shift-key and used free-text search so we remove + // the remaining part + const parseRes = Parser.parse(this.textSearch); + let query = ""; + objKeys(parseRes.conditions).forEach(field => { + parseRes.conditions[field]?.forEach(value => { + query += `${field}:${JSON.stringify(value)} ` + }) + }) + this.textSearch = query; + } + + if (event.shiftKey) { + const textqlVal = `${field}:${JSON.stringify(val)}` + if (!this.textSearch.includes(textqlVal)) { + if (this.textSearch !== '') { + this.textSearch += " " + } + this.textSearch += textqlVal + " " + this.triggerDropdownClose$.next(false) + // load new suggestions based on the new input + this.loadSuggestions(); + } + + return; + } + + // directly emit the new value and reset the text search + this.fieldsParsed.next({ + [field]: [val] + }) + + // parse and emit the current search field but without the suggestion value + this.parseAndEmit(); + + this.suggestionDropDown?.close(); + + this.cdr.markForCheck(); + } + + resetKeyboardSelection() { + this.keyManager.setActiveItem(0); + } + + loadSuggestions() { + this.loading = true; + this.loadSuggestions$.next(); + this.suggestionDropDown?.show(this.searchBoxOverlayOrigin) + } + + trackSuggestion: TrackByFunction> = (_: number, val: SfngSearchbarSuggestion) => val.field; + + constructor( + private cdr: ChangeDetectorRef, + private expertiseService: ExpertiseService, + private netquery: Netquery, + private helper: NetqueryHelper, + ) { } +} diff --git a/desktop/angular/src/app/shared/netquery/tag-bar/index.ts b/desktop/angular/src/app/shared/netquery/tag-bar/index.ts new file mode 100644 index 00000000..3439acb3 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/tag-bar/index.ts @@ -0,0 +1 @@ +export * from './tag-bar'; diff --git a/desktop/angular/src/app/shared/netquery/tag-bar/tag-bar.html b/desktop/angular/src/app/shared/netquery/tag-bar/tag-bar.html new file mode 100644 index 00000000..f1161e58 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/tag-bar/tag-bar.html @@ -0,0 +1,26 @@ +
+ +
+ + + {{labels[cat.key] || cat.key}}: + + + + + {{ val.Name || (val.Value === '' ? 'N/A' : val) }} + + + + + + + + +
+
+
diff --git a/desktop/angular/src/app/shared/netquery/tag-bar/tag-bar.ts b/desktop/angular/src/app/shared/netquery/tag-bar/tag-bar.ts new file mode 100644 index 00000000..bbff7417 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/tag-bar/tag-bar.ts @@ -0,0 +1,136 @@ +import { coerceBooleanProperty, coerceCssPixelValue } from '@angular/cdk/coercion'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, HostBinding, Input } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { PossilbeValue } from '@safing/portmaster-api'; +import { fadeInListAnimation } from '../../animations'; +import { NetqueryHelper } from '../connection-helper.service'; + +export interface SfngTagbarValue { + key: string; + values: PossilbeValue[]; +} + +@Component({ + selector: 'sfng-netquery-tagbar', + templateUrl: 'tag-bar.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styles: [ + ` + :host { + @apply flex flex-row gap-3 w-auto items-center text-xxs flex-wrap; + } + ` + ], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SfngNetqueryTagbarComponent), + multi: true + } + ], + animations: [ + fadeInListAnimation + ] +}) +export class SfngNetqueryTagbarComponent implements ControlValueAccessor { + @HostBinding('@fadeInList') + get itemsLength() { + return this.values?.length || 0; + } + + /** @private the current tag bar values */ + values: SfngTagbarValue[] = []; + + /** Whether or not the user can interact with the component */ + @Input() + set disabled(v: any) { + this.setDisabledState(v) + } + get disabled() { + return this._disabled; + } + private _disabled = false; + + /** Translations for the value keys */ + @Input() + labels: { [key: string]: string } = {} + + /** The maximum width of the tag text before being truncated using left-side ellipsis */ + @Input() + set maxTagWidth(width: any) { + this._maxTagWidth = coerceCssPixelValue(width) + } + get maxTagWidth() { + return this._maxTagWidth + } + private _maxTagWidth: string = '8rem' + + /** @private A {@link TrackByFunction} for {@link SfngTagbarValue} */ + trackValue(_: number, vl: SfngTagbarValue) { + return vl.key; + } + + /** Implements the {@link ControlValueAccessor} */ + writeValue(obj: SfngTagbarValue[]): void { + this.values = obj; + this.cdr.markForCheck(); + } + + /** Implements the {@link ControlValueAccessor} */ + registerOnChange(fn: any): void { + this._onChange = fn; + } + + /** @private - callback registered via registerOnChange */ + _onChange: (val: SfngTagbarValue[]) => void = () => { } + + /** Implements the {@link ControlValueAccessor} */ + registerOnTouched(fn: any): void { + this._onTouched = fn + } + + /** @private - callback registered via registerOnTouched */ + _onTouched: () => void = () => { } + + /** Implements the {@link ControlValueAccessor} */ + setDisabledState(v: any) { + this._disabled = coerceBooleanProperty(v) + this.cdr.markForCheck(); + } + + /** + * remove removes the value at index from the {@link SfngTagbarValue} + * that matches key. + */ + remove(key: string, index: number) { + if (this.disabled) { + return; + } + + console.log(this.values); + + let cpy: SfngTagbarValue[] = []; + + this.values.forEach(val => { + if (val.key === key) { + val.values = [...val.values]; + val.values.splice(index, 1) + } + cpy.push({ + ...val, + }) + }); + + this.values = cpy; + + console.log(this.values); + + this._onChange(this.values); + this.cdr.markForCheck(); + } + + constructor( + private cdr: ChangeDetectorRef, + private helper: NetqueryHelper, + ) { } +} diff --git a/desktop/angular/src/app/shared/netquery/textql/helper.ts b/desktop/angular/src/app/shared/netquery/textql/helper.ts new file mode 100644 index 00000000..8f523aaa --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/textql/helper.ts @@ -0,0 +1,21 @@ +import { Token, TokenType } from "./token"; + +export function isValueToken(tok: Token): tok is Token { + return [TokenType.STRING, TokenType.BOOL, TokenType.NUMBER].includes(tok.type) +} + +export function isDigit(x: string): boolean { + return /[0-9]+/.test(x); +} + +export function isWhitespace(ch: string): boolean { + return /\s/.test(ch) +} + +export function isLetter(ch: string): boolean { + return new RegExp('[\/a-zA-Z0-9\._-]').test(ch) +} + +export function isIdentChar(ch: string): boolean { + return /[a-zA-Z_]/.test(ch); +} diff --git a/desktop/angular/src/app/shared/netquery/textql/index.ts b/desktop/angular/src/app/shared/netquery/textql/index.ts new file mode 100644 index 00000000..75aa8b93 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/textql/index.ts @@ -0,0 +1 @@ +export * from './parser'; diff --git a/desktop/angular/src/app/shared/netquery/textql/input.ts b/desktop/angular/src/app/shared/netquery/textql/input.ts new file mode 100644 index 00000000..4180d193 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/textql/input.ts @@ -0,0 +1,41 @@ +/** Input stream returns one character at a time */ +export class InputStream { + private _pos: number = 0; + private _line: number = 0; + + constructor(private _input: string) { } + + /** Returns the next character and removes it from the stream */ + next(): string | null { + const ch = this._input.charAt(this._pos++); + return ch; + } + + get pos() { + return this._pos; + } + + /** Revert moves the current stream position back by `num` characters */ + revert(num: number) { + this._pos -= num; + } + + /** Returns the next character in the stream but does not remove it */ + peek(): string { + return this._input.charAt(this._pos); + } + + /** Returns true if we reached the end of the stream */ + eof(): boolean { + return this.peek() == ''; + } + + get left(): string { + return this._input.slice(this._pos) + } + + /** Throws an error with the current line and column */ + croak(msg: string): never { + throw new Error(`${msg} at ${this._line}:${this.pos}`); + } +} diff --git a/desktop/angular/src/app/shared/netquery/textql/lexer.ts b/desktop/angular/src/app/shared/netquery/textql/lexer.ts new file mode 100644 index 00000000..008cbd6e --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/textql/lexer.ts @@ -0,0 +1,255 @@ +import { isDigit, isIdentChar, isLetter, isWhitespace } from "./helper"; +import { InputStream } from "./input"; +import { Token, TokenType } from "./token"; + +export class Lexer { + private _current: Token | null = null; + private _input: InputStream; + + constructor(input: string) { + this._input = new InputStream(input); + } + + /** peek returns the token at the current position in input. */ + public peek(): Token | null { + return this._current || (this._current = this.readNextToken()); + } + + /** next returns either the current token in input or reads the next one */ + public next(): Token | null { + let tok = this._current; + this._current = null; + return tok || this.readNextToken(); + } + + /** eof returns true if the lexer reached the end of the input stream */ + public eof(): boolean { + return this.peek() === null; + } + + /** croak throws and error message at the current position in the input stream */ + public croak(msg: string): never { + return this._input.croak(`${msg}. Current token is "${!!this.peek() ? this.peek()!.literal : null}"`); + } + + /** consumes the input stream as long as predicate returns true */ + private readWhile(predicate: (ch: string) => boolean): string { + let str = ''; + while (!this._input.eof() && predicate(this._input.peek())) { + str += this._input.next(); + } + + return str; + } + + /** reads a number token */ + private readNumber(): Token { + const start = this._input.pos; + + let has_dot = false; + let number = this.readWhile((ch: string) => { + if (ch === '.') { + if (has_dot) { + return false; + } + + has_dot = true; + return true; + } + return isDigit(ch); + }); + + if (!this._input.eof() && isIdentChar(this._input.peek())) { + this._input.revert(number.length + 1); + this._input.croak("invalid number character") + } + + return { + type: TokenType.NUMBER, + literal: number, + value: has_dot ? parseFloat(number) : parseInt(number), + start + } + } + + private readIdent(): Token { + const start = this._input.pos; + + const id = this.readWhile(ch => isIdentChar(ch)); + if (id === 'true' || id === 'yes') { + return { + type: TokenType.BOOL, + literal: id, + value: true, + start + } + } + if (id === 'false' || id === 'no') { + return { + type: TokenType.BOOL, + literal: id, + value: false, + start + } + } + if (id === 'groupby') { + return { + type: TokenType.GROUPBY, + literal: id, + value: id, + start + } + } + if (id === 'orderby') { + return { + type: TokenType.ORDERBY, + literal: id, + value: id, + start + } + } + + return { + type: TokenType.IDENT, + literal: id, + value: id, + start + }; + } + + private readEscaped(end: string | RegExp, skipStart: boolean): string { + let escaped = false; + let str = ''; + + if (skipStart) { + this._input.next(); + } + + while (!this._input.eof()) { + let ch = this._input.next()!; + if (escaped) { + str += ch; + escaped = false; + } else if (ch === '\\') { + escaped = true; + } else if ((typeof end === 'string' && ch === end) || (end instanceof RegExp && end.test(ch))) { + break; + } else { + str += ch; + } + } + return str; + } + + private readString(quote: string | RegExp, skipStart: boolean): Token { + const start = this._input.pos; + const value = this.readEscaped(quote, skipStart) + return { + type: TokenType.STRING, + literal: value, + value: value, + start + } + } + + private readWhitespace(): Token { + const start = this._input.pos; + const value = this.readWhile(ch => isWhitespace(ch)); + return { + type: TokenType.WHITESPACE, + literal: value, + value: value, + start, + } + } + + private readNextToken(): Token | null { + const start = this._input.pos; + const ch = this._input.peek(); + if (ch === '') { + return null; + } + + if (isWhitespace(ch)) { + return this.readWhitespace() + } + + if (ch === '"') { + return this.readString('"', true); + } + + if (ch === '\'') { + return this.readString('\'', true); + } + + try { + if (isDigit(ch)) { + return this.readNumber(); + } + } catch (err) { + // we ignore that error here as it may only happen for unqoted strings + // that start with a number. + } + + if (ch === ':') { + this._input.next(); + return { + type: TokenType.COLON, + value: ':', + literal: ':', + start + } + } + + if (ch === '!') { + this._input.next(); + return { + type: TokenType.NOT, + value: '!', + literal: '!', + start + } + } + + if (isIdentChar(ch)) { + const ident = this.readIdent(); + + const next = this._input.peek(); + if (!this._input.eof() && (!isWhitespace(next) && next !== ':')) { + + // identifiers should always end in a colon or with a whitespace. + // if neither is the case we are in the middle of a token and are + // likely parsing a string without quotes. + this._input.revert(ident.literal.length); + + // read the string and revert by one as we terminate the string + // at the next WHITESPACE token + const tok = this.readString(new RegExp('\\s'), false) + this.revertWhitespace(); + + return tok; + } + + return ident; + } + + if (isLetter(ch)) { + const tok = this.readString(new RegExp('\\s'), false) + // read the string and revert by one as we terminate the string + // at the next WHITESPACE token + this.revertWhitespace(); + + return tok + } + + // Failed to handle the input character + return this._input.croak(`Can't handle character: ${ch}`); + } + + private revertWhitespace() { + this._input.revert(1) + if (!isWhitespace(this._input.peek())) { + this._input.next(); + } + } +} diff --git a/desktop/angular/src/app/shared/netquery/textql/parser.ts b/desktop/angular/src/app/shared/netquery/textql/parser.ts new file mode 100644 index 00000000..5cf492f7 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/textql/parser.ts @@ -0,0 +1,204 @@ +import { isDevMode } from '@angular/core'; +import { isValueToken, isWhitespace } from './helper'; +import { Lexer } from './lexer'; +import { Token, TokenType } from './token'; + + +export interface ParseResult { + conditions: { + [key: string]: (any | { $ne: any })[]; + }; + textQuery: string; + groupBy?: string[]; + orderBy?: string[]; +} + +export class Parser { + /** The underlying lexer used to tokenize the input */ + private lexer: Lexer; + + /** Holds the parsed conditions */ + private conditions: { + [key: string]: any[]; + } = {}; + + /** The last condition that has not yet been terminated. Used for scope-based suggestions */ + private _lastUnterminatedCondition: { + start: number; + type: string; + value: any; + } | null = null; + + /** A list of remaining strings/identifiers that are not part of a condition */ + private remaining: string[] = []; + + /** Returns the last condition that has not yet been terminated. */ + get lastUnterminatedCondition() { + return this._lastUnterminatedCondition; + } + + constructor(input: string) { + this.lexer = new Lexer(input); + } + + static aliases: { [key: string]: string } = { + 'provider': 'as_owner', + 'app': 'profile', + 'ip': 'remote_ip', + 'port': 'remote_port' + } + + /** parse is a shortcut for new Parser(input).process() */ + static parse(input: string): ParseResult { + return new Parser(input).process(); + } + + /** Process the whole input stream and return the parsed result */ + process(): ParseResult { + let lastIdent: Token | null = null; + let hasColon = false; + let not = false; + let groupBy: string[] = []; + let orderBy: string[] = []; + + while (true) { + const tok = this.lexer.next() + if (tok === null) { + break; + } + + if (isDevMode()) { + console.log(tok) + } + + // if we find a whitespace token we count it as a termination character + // for the last unterminated condition. + if (tok.type === TokenType.WHITESPACE) { + this._lastUnterminatedCondition = null; + } + + // Since we allow the user to enter values without quotes the + // lexer might wrongly declare a "string value" as an IDENT. + // If we have the pattern we re-classify + // the last IDENT as a STRING value + if (!!lastIdent && hasColon && tok.type === TokenType.IDENT) { + tok.type = TokenType.STRING; + } + + if (tok.type === TokenType.IDENT || tok.type === TokenType.GROUPBY || tok.type === TokenType.ORDERBY) { + // if we had an IDENT token before and got a new one now the + // previous one is pushed to the remaining list + if (!!lastIdent) { + this._lastUnterminatedCondition = null; + this.remaining.push(lastIdent.value) + } + lastIdent = tok; + this._lastUnterminatedCondition = { + start: tok.start, + type: Parser.aliases[lastIdent.value] || lastIdent.value, + value: '', + } + + continue + } + + // if we don't have an preceding IDENT token + // this must be part of remaingin + if (!lastIdent) { + this.remaining.push(tok.literal); + this._lastUnterminatedCondition = null; + + continue + } + + // we would expect a colon now + if (!hasColon) { + if (tok.type !== TokenType.COLON) { + // we expected a colon but got something else. + // this means the last IDENT is part of remaining + this.remaining.push(lastIdent.value); + lastIdent = null; + this._lastUnterminatedCondition = null; + + continue + } + + // we have a colon now so proceed to the next token + hasColon = true; + not = false; + + continue + } + + if (lastIdent.type === TokenType.GROUPBY) { + groupBy.push(Parser.aliases[tok.literal] || tok.literal) + lastIdent = null + hasColon = false + + continue + } + + if (lastIdent.type == TokenType.ORDERBY) { + orderBy.push(Parser.aliases[tok.literal] || tok.literal) + lastIdent = null + hasColon = false + + continue + } + + if (tok.type === TokenType.NOT && not === false) { + not = true + + continue + } + + if (isValueToken(tok)) { + let identValue = Parser.aliases[lastIdent.value] || lastIdent.value; + + if (!this.conditions[identValue]) { + this.conditions[identValue] = []; + } + + if (!not) { + this.conditions[identValue].push(tok.value) + } else { + this.conditions[identValue].push({ $ne: tok.value }) + } + this._lastUnterminatedCondition!.value = tok.value; + + lastIdent = null + hasColon = false + not = false + + continue + } + + this.remaining.push(lastIdent.value); + lastIdent = null; + hasColon = false; + not = false; + this._lastUnterminatedCondition = null; + } + + if (!!lastIdent) { + this.remaining.push(lastIdent.value); + + if (hasColon) { + this._lastUnterminatedCondition = { + start: lastIdent.start, + type: Parser.aliases[lastIdent.value] || lastIdent.value, + value: '' + }; + } else { + this._lastUnterminatedCondition = null; + } + } + + return { + groupBy: groupBy.length > 0 ? groupBy : undefined, + orderBy: orderBy.length > 0 ? orderBy : undefined, + conditions: this.conditions, + textQuery: this.remaining.filter(tok => !isWhitespace(tok)).join(" "), + } + } +} diff --git a/desktop/angular/src/app/shared/netquery/textql/token.ts b/desktop/angular/src/app/shared/netquery/textql/token.ts new file mode 100644 index 00000000..ae9039d7 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/textql/token.ts @@ -0,0 +1,46 @@ + +/** + * Language Definition: + * + * input: + * + * [EXPR] [EXPR]... + * + * with: + * + * EXPR = [IDENT][COLON][NOT?][VALUE] + * NOT = "!" + * VALUE = [STRING][BOOL][NUMBER] + * STRING = [a-zA-Z\.0-9] + * BOOL = true | false + * NUMBER = [0-9]+ + * COLON = ":" + * + */ + +export enum TokenType { + WHITESPACE = 'WHITESPACE', + IDENT = 'IDENT', + COLON = 'COLON', + STRING = 'STRING', + NUMBER = 'NUMBER', + BOOL = 'BOOL', + NOT = 'NOT', + GROUPBY = 'GROUPBY', + ORDERBY = 'ORDERBY' +} + +export type TokenValue = + T extends TokenType.NUMBER ? number : + T extends TokenType.STRING ? string : + T extends TokenType.BOOL ? boolean : + T extends TokenType.NOT ? '!' : + T extends TokenType.GROUPBY ? 'string' : + string; + +export interface Token { + type: T; + literal: string; + value: TokenValue; + start: number; +} diff --git a/desktop/angular/src/app/shared/netquery/utils.ts b/desktop/angular/src/app/shared/netquery/utils.ts new file mode 100644 index 00000000..95f15034 --- /dev/null +++ b/desktop/angular/src/app/shared/netquery/utils.ts @@ -0,0 +1,63 @@ +import { Condition, Matcher } from "@safing/portmaster-api"; +import { objKeys } from "../utils"; + +export const connectionFieldTranslation: { [key: string]: string } = { + domain: "Domain", + profile: "App", + path: 'Binary Path', + scope: 'Scope', + as_owner: "Provider", + country: "Country", + direction: 'Direction', + started: 'Started', + ended: 'Ended', + remote_ip: 'Remote IP', + verdict: 'Verdict', + encrypted: 'Encrypted', + internal: 'Internal', + asn: 'ASN', + tunneled: 'SPN Active', + active: 'Active', + allowed: 'Allowed', + from: 'From', + to: 'To', + remote_port: 'Port', + bytes_sent: 'Bytes Sent', + bytes_received: 'Bytes Received' +} + +export function isMatcher(v: any | Matcher): v is Matcher { + return typeof v === 'object' && ('$eq' in v || '$ne' in v || '$like' in v || '$in' in v || '$notin' in v); +} + +export function mergeConditions(cond1: Condition, cond2: Condition): Condition { + const result: Condition = {}; + + objKeys(cond1).forEach(key => { + let val = cond1[key]; + if (Array.isArray(val)) { + result[key] = val; + } else { + result[key] = [val]; + } + }) + + objKeys(cond2).forEach(key => { + let val = cond2[key]; + if (!Array.isArray(val)) { + val = [val] + } + + if (!(key in result)) { + result[key] = val; + } else { + result[key] = [ + ...(result[key] as any), // this must be an array here + ...val, + ] + } + }) + + + return result; +} diff --git a/desktop/angular/src/app/shared/network-scout/index.ts b/desktop/angular/src/app/shared/network-scout/index.ts new file mode 100644 index 00000000..fa8417ee --- /dev/null +++ b/desktop/angular/src/app/shared/network-scout/index.ts @@ -0,0 +1 @@ +export * from './network-scout'; diff --git a/desktop/angular/src/app/shared/network-scout/network-scout.html b/desktop/angular/src/app/shared/network-scout/network-scout.html new file mode 100644 index 00000000..b314d86b --- /dev/null +++ b/desktop/angular/src/app/shared/network-scout/network-scout.html @@ -0,0 +1,182 @@ +
+
+ + + + + + {{allProfiles.length}} Apps + +
+ + + + + + + + +
+

Sort By

+ + + + {{ sortMethod }} + + +
+
+ + + + + + + + + +
+ + + +
+ {{ profile.exitPins.length }} + IDENTITIES +
+ + + Connections from {{ profile.Name }} have not been routed through the SPN. + + +
    +
  • + + + + {{ entity.IP }} + + +
    + + {{ identity.count }} + Connections + + + HOPS: + {{ identity.HopDistance }} + +
    +
  • +
+ + + {{ profile.showMore ? 'Show Less Identities' : 'Show More Identities'}} + +
+
+ + +
+ + + + + + + + + {{ data.Name }} + + + + +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + +
+ +
+ + + +
+ + + + + {{ data.bytes_sent | bytes:"1.0-0" }} + + + + + + + {{ data.bytes_received | bytes:"1.0-0" }} + +
+
+
+
diff --git a/desktop/angular/src/app/shared/network-scout/network-scout.scss b/desktop/angular/src/app/shared/network-scout/network-scout.scss new file mode 100644 index 00000000..9f24cfef --- /dev/null +++ b/desktop/angular/src/app/shared/network-scout/network-scout.scss @@ -0,0 +1,3 @@ +:host { + @apply w-full p-2 flex flex-col gap-2; +} diff --git a/desktop/angular/src/app/shared/network-scout/network-scout.ts b/desktop/angular/src/app/shared/network-scout/network-scout.ts new file mode 100644 index 00000000..8c3c7d88 --- /dev/null +++ b/desktop/angular/src/app/shared/network-scout/network-scout.ts @@ -0,0 +1,322 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, OnInit, TrackByFunction, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { BoolSetting, Condition, ConfigService, ExpertiseLevel, IProfileStats, Netquery, Pin, SPNService } from "@safing/portmaster-api"; +import { Subject, combineLatest, debounceTime, filter, finalize, interval, retry, startWith, switchMap, take, takeUntil } from "rxjs"; +import { UIStateService } from "src/app/services"; +import { fadeInListAnimation } from "../animations"; +import { ExpertiseService } from './../expertise/expertise.service'; + +interface _Pin extends Pin { + count: number; +} + +interface _Profile extends IProfileStats { + exitPins: _Pin[]; + showMore: boolean; + expanded: boolean; +} + +export enum SortTypes { + static = 'Static', + aToZ = "A-Z", + zToA = "Z-A", + totalConnections = "Total Connections", + connectionsDenied = "Denied Connections", + connectionsAllowed = "Allowed Connections", + spnIdentities = "SPN Identities", + bytesSent = "Bytes Sent", + bytesReceived = "Bytes Received", + totalBytes = "Total Bytes" +} + +const bandwidthSorts: SortTypes[] = [ + SortTypes.bytesReceived, + SortTypes.bytesSent, + SortTypes.totalBytes +] + +@Component({ + selector: 'app-network-scout', + templateUrl: './network-scout.html', + styleUrls: ['./network-scout.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + fadeInListAnimation, + ] +}) +export class NetworkScoutComponent implements OnInit { + private destroyRef = inject(DestroyRef); + + sortTypes = [ + SortTypes.static, + SortTypes.aToZ, + SortTypes.zToA, + SortTypes.totalConnections, + SortTypes.connectionsDenied, + SortTypes.connectionsAllowed, + SortTypes.spnIdentities + ] + + readonly sortMethods = new Map([ + // there's not entry for "Static" here on purpose because we'll use the sort order + // returned by netquery. + [SortTypes.aToZ, (a: _Profile, b: _Profile) => a.Name.localeCompare(b.Name)], + [SortTypes.zToA, (a: _Profile, b: _Profile) => b.Name.localeCompare(a.Name)], + [SortTypes.totalConnections, (a: _Profile, b: _Profile) => (b.countAllowed + b.countUnpermitted) - (a.countAllowed + a.countUnpermitted)], + [SortTypes.connectionsAllowed, (a: _Profile, b: _Profile) => b.countAllowed - a.countAllowed], + [SortTypes.connectionsDenied, (a: _Profile, b: _Profile) => b.countUnpermitted - a.countUnpermitted], + [SortTypes.spnIdentities, (a: _Profile, b: _Profile) => a.identities.length - b.identities.length], + [SortTypes.bytesReceived, (a: _Profile, b: _Profile) => b.bytes_received - a.bytes_received], + [SortTypes.bytesSent, (a: _Profile, b: _Profile) => b.bytes_sent - a.bytes_sent], + [SortTypes.totalBytes, (a: _Profile, b: _Profile) => (b.bytes_received + b.bytes_sent) - (a.bytes_received + a.bytes_sent)] + ]); + + /** The current sort order */ + sortOrder: SortTypes = SortTypes.static; + + get isByteSortOrder() { + return bandwidthSorts.includes(this.sortOrder); + } + + /** Used to trigger a debounced search from the template */ + triggerSearch = new Subject(); + + /** The current search term as entered in the input[type="text"] */ + searchTerm: string = ''; + + /** A list of all active profiles without any search applied */ + allProfiles: _Profile[] = []; + + /** Defines if new elements should be expanded or collapsed */ + expandCollapseState: 'expand' | 'collapse' = 'expand'; + + /** Whether or not the SPN is enabled */ + spnEnabled = false; + + /** + * Emits when the user clicks the "expand all" or "collapse all" buttons. + * Once the user did that we stop updating the default state depending on whether the + * SPN is enabled or not. + */ + private userChangedState = new Subject(); + + /** + * A list of profiles that are currently displayed. This is basically allProfiles but with + * text search applied. + */ + profiles: _Profile[] = []; + + /** TrackByFunction for the profiles. */ + trackProfile: TrackByFunction<_Profile> = (_, profile) => profile.ID; + + /** TrackByFunction for the exit pins */ + trackPin: TrackByFunction<_Pin> = (_, pin) => pin.ID; + + constructor( + private netquery: Netquery, + private spn: SPNService, + private configService: ConfigService, + private stateService: UIStateService, + private expertise: ExpertiseService, + private cdr: ChangeDetectorRef, + ) { } + + searchProfiles(term: string) { + term = term.trim(); + + if (term === '') { + this.profiles = [ + ...this.allProfiles + ]; + + this.sortProfiles(this.profiles); + + return; + } + + const lowerCaseTerm = term.toLocaleLowerCase() + this.profiles = this.allProfiles.filter(p => { + if (p.ID.toLocaleLowerCase().includes(lowerCaseTerm)) { + return true; + } + + if (p.Name.toLocaleLowerCase().includes(lowerCaseTerm)) { + return true; + } + + if (p.exitPins.some(pin => pin.Name.toLocaleLowerCase().includes(lowerCaseTerm))) { + return true; + } + + return false; + }) + + this.sortProfiles(this.profiles); + } + + sortProfiles(profiles: _Profile[]) { + const method = this.sortMethods.get(this.sortOrder); + if (!method) { + return; + } + + profiles.sort(method) + + this.cdr.markForCheck(); + } + + updateSortOrder(newOrder: SortTypes) { + this.sortOrder = newOrder; + this.searchProfiles(this.searchTerm); + + this.stateService.set('netscoutSortOrder', newOrder) + .subscribe({ + error: err => { + console.error(err); + } + }) + } + + expandAll() { + this.expandCollapseState = 'expand'; + this.allProfiles.forEach(profile => profile.expanded = profile.identities.length > 0) + this.searchProfiles(this.searchTerm) + this.userChangedState.next(); + + this.cdr.markForCheck() + } + + collapseAll() { + this.expandCollapseState = 'collapse'; + this.allProfiles.forEach(profile => profile.expanded = false) + this.searchProfiles(this.searchTerm) + this.userChangedState.next(); + + this.cdr.markForCheck() + } + + ngOnInit(): void { + this.stateService.uiState() + .pipe(take(1)) + .subscribe(state => { + this.sortOrder = state.netscoutSortOrder; + + this.searchProfiles(this.searchTerm); + }) + + this.configService.watch('spn/enable') + .pipe( + takeUntilDestroyed(this.destroyRef), + takeUntil(this.userChangedState), + ) + .subscribe(enabled => { + // if the SPN is enabled and the user did not yet change the + // collapse/expand state we switch to "expand" for the default. + // Otherwise, there will be no identities so there's no reason + // to expand them at all so we switch to collapse + if (enabled) { + this.expandCollapseState = 'expand' + } else { + this.expandCollapseState = 'collapse' + } + + this.spnEnabled = enabled; + }); + + let updateInProgress = false; + + combineLatest([ + combineLatest([ + interval(5000) + .pipe( + filter(() => !updateInProgress) + ), + this.expertise.change, + ]) + .pipe( + startWith(-1), + switchMap(() => { + let query: Condition = {}; + if (this.expertise.currentLevel !== ExpertiseLevel.Developer) { + query["internal"] = { $eq: false } + } + + updateInProgress = true + + return this.netquery.getProfileStats(query) + .pipe( + finalize(() => updateInProgress = false) + ) + }), + retry({ delay: 5000 }) + ), + + this.spn.watchPins() + .pipe( + debounceTime(100), + startWith([]), + ), + + this.triggerSearch + .pipe( + debounceTime(100), + startWith(''), + ), + ]) + .pipe( + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(([res, pins, searchTerm]) => { + // create a lookup map for the the SPN map pins + const pinLookupMap = new Map(); + pins.forEach(p => pinLookupMap.set(p.ID, p)) + + // create a lookup map from already known profiles so we can + // inherit states like "showMore". + const profileLookupMap = new Map(); + this.allProfiles.forEach(p => profileLookupMap.set(p.ID, p)) + + // map the list of profile statistics to include the exit Pin information + // as well. + this.allProfiles = res.map(s => { + const existing = profileLookupMap.get(s.ID); + return { + ...s, + exitPins: s.identities + .map(ident => { + const pin = pinLookupMap.get(ident.exit_node); + if (!pin) { + return null; + } + + return { + count: ident.count, + ...pin + } + }) + .filter(pin => !!pin), + showMore: existing?.showMore ?? false, + expanded: existing?.expanded ?? (this.expandCollapseState === 'expand' && s.identities.length > 1 /* there's always the "direct" identity */), + } as _Profile + }); + + this.searchProfiles(searchTerm); + + // check if we have profiles with bandwidth data and + // make sure our sort methods are updated. + if (this.profiles.some(p => p.bytes_received > 0 || p.bytes_sent > 0)) { + if (!this.sortTypes.includes(SortTypes.bytesReceived)) { + this.sortTypes.push.apply(this.sortTypes, bandwidthSorts) + } + + this.sortTypes = [...this.sortTypes]; + } else { + this.sortTypes = this.sortTypes.filter(type => { + return !bandwidthSorts.includes(type) + }) + } + + this.cdr.markForCheck(); + }) + } +} diff --git a/desktop/angular/src/app/shared/notification-list/index.ts b/desktop/angular/src/app/shared/notification-list/index.ts new file mode 100644 index 00000000..3fa640e3 --- /dev/null +++ b/desktop/angular/src/app/shared/notification-list/index.ts @@ -0,0 +1 @@ +export { NotificationListComponent as NotificationWidgetComponent, NotificationWidgetConfig } from './notification-list.component'; diff --git a/desktop/angular/src/app/shared/notification-list/notification-list.component.html b/desktop/angular/src/app/shared/notification-list/notification-list.component.html new file mode 100644 index 00000000..23f2cb08 --- /dev/null +++ b/desktop/angular/src/app/shared/notification-list/notification-list.component.html @@ -0,0 +1,24 @@ +Notifications + +
+
+
+ + + +
+
+ + {{notif.Title || notif.Message}} + +
+ +
+
+
+
diff --git a/desktop/angular/src/app/shared/notification-list/notification-list.component.scss b/desktop/angular/src/app/shared/notification-list/notification-list.component.scss new file mode 100644 index 00000000..e99c059e --- /dev/null +++ b/desktop/angular/src/app/shared/notification-list/notification-list.component.scss @@ -0,0 +1,186 @@ +:host { + @apply flex flex-col justify-start items-center gap-2; + @apply w-full px-2; + + @apply border-b border-gray-400 pb-2; + + &>* { + /* do not allow to shrink */ + flex-shrink: 0; + } +} + +.row, +div.placeholder { + display: flex; + flex-direction: column; + width: 100%; + margin: 0; + border: none; +} + +.row { + @apply overflow-hidden w-full flex flex-row rounded; + @apply h-8; + + .type { + display: flex; + justify-content: center; + align-items: center; + width: .5rem; + flex-shrink: 0; + flex-grow: 0; + background-color: #202020; + + &.info { + background-color: #727272; + } + + &.warning { + background-color: theme("colors.info.yellow"); + } + + &.error { + background-color: theme("colors.info.red"); + } + + &.broadcast { + width: 2rem; + color: #00000080; + } + } + + .preview { + background-color: #292929; + cursor: pointer; + overflow: hidden; + flex-grow: 1; + display: flex; + justify-content: space-between; + align-items: center; + padding-left: 1rem; + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; + position: relative; + + span { + flex-grow: 1; + text-overflow: ellipsis; + overflow: hidden; + word-wrap: none; + white-space: nowrap; + + font-size: 0.7rem; + font-weight: 500; + + color: #cacaca; + + .category { + padding-left: 8px; + font-size: 0.65rem; + font-weight: 700; + text-transform: capitalize; + color: #999999c9; + } + } + + &:hover { + background-color: #303030; + + .buttons { + opacity: 1; + transition: all .05s ease-in-out; + transform: translateX(-100%); + } + } + + .buttons { + opacity: 0; + transition: all .05s ease-in-out; + height: 100%; + position: absolute; + left: 100%; + display: flex; + white-space: nowrap; + background-color: #303030; + + button { + outline: none; + @apply bg-transparent; + font-size: 0.6rem; + background-color: #3a3a3a; + padding-left: 1.25rem; + padding-right: 1.25rem; + text-transform: capitalize; + border-radius: 0; + font-weight: 500; + outline: none; + color: hsla(0, 0%, 100%, 0.548); + height: 100%; + + &:hover { + background-color: #363636; + color: #ffffff; + } + + &:first-of-type { + margin-left: .5rem; + } + + &:last-of-type { + background: transparent; + color: hsla(0, 0%, 100%, 0.562); + @apply ml-1; + transition: all cubic-bezier(0.175, 0.885, 0.32, 1.275) .2s; + + &:hover { + color: #ffffff; + } + } + } + } + } +} + +/* +.notification-body { + @apply bg-cards-tertiary; + flex-grow: 1; + @apply rounded-b; + position: absolute; + top: var(--slot-size); + bottom: 0; + + .broadcast-info { + background-color: #00000040; + width: 100%; + padding: 0.5rem; + color: white !important; + font-weight: 400; + bottom: 0; + position: absolute; + flex-grow: 1; + @apply flex items-center justify-center gap-1; + } +} +*/ + +div.placeholder { + @apply font-medium; + @apply text-tertiary; + @apply flex-grow; + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + user-select: none; +} + +app-loading { + opacity: .5; + margin-left: auto; + margin-right: auto; + position: relative; + top: 5px; +} diff --git a/desktop/angular/src/app/shared/notification-list/notification-list.component.ts b/desktop/angular/src/app/shared/notification-list/notification-list.component.ts new file mode 100644 index 00000000..d54e2b98 --- /dev/null +++ b/desktop/angular/src/app/shared/notification-list/notification-list.component.ts @@ -0,0 +1,138 @@ +import { animate, style, transition, trigger } from '@angular/animations'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostBinding, OnDestroy, OnInit, TrackByFunction, inject } from '@angular/core'; +import { SfngDialogService } from '@safing/ui'; +import { Subscription } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { Action, Notification, NotificationType, NotificationsService } from 'src/app/services'; +import { moveInOutAnimation, moveInOutListAnimation } from 'src/app/shared/animations'; +import { NotificationComponent } from '../notification/notification'; + +export interface NotificationWidgetConfig { + markdown: boolean; +} + +export interface _Notification extends Notification { + isBroadcast: boolean +} + +@Component({ + selector: 'app-notification-list', + templateUrl: './notification-list.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: [ + './notification-list.component.scss' + ], + animations: [ + trigger( + 'fadeIn', + [ + transition( + ':enter', + [ + style({ opacity: 0 }), + animate('.2s .2s ease-in', + style({ opacity: 1 })) + ] + ), + ] + ), + moveInOutAnimation, + moveInOutListAnimation + ] +}) +export class NotificationListComponent implements OnInit, OnDestroy { + readonly types = NotificationType; + readonly dialog = inject(SfngDialogService); + readonly cdr = inject(ChangeDetectorRef); + + /** Used to set a fixed height when a notification is expanded. */ + @HostBinding('style.height') + height: null | string = null; + + /** Sets the overflow to hidden when a notification is expanded. */ + @HostBinding('style.overflow') + get overflow() { + if (this.height === null) { + return null; + } + return 'hidden'; + } + + @HostBinding('class.empty') + get isEmpty() { + return this.notifications.length === 0; + } + + @HostBinding('@moveInOutList') + get length() { return this.notifications.length } + + /** Subscription to notification updates. */ + private notifSub = Subscription.EMPTY; + + /** All active notifications. */ + notifications: _Notification[] = []; + + trackBy: TrackByFunction<_Notification> = this.notifsService.trackBy; + + constructor( + public elementRef: ElementRef, + public notifsService: NotificationsService, + ) { } + + ngOnInit(): void { + this.notifSub = this.notifsService + .new$ + .pipe( + // filter out any prompts as they are handled by a different widget. + map(notifs => { + return notifs.filter(notif => !notif.SelectedActionID && !(notif.Type === NotificationType.Prompt && notif.EventID.startsWith("filter:prompt"))) + }) + ) + .subscribe(list => { + this.notifications = list.map(notification => { + return { + ...notification, + isBroadcast: notification.EventID.startsWith("broadcasts:"), + } + }); + + this.cdr.markForCheck(); + }); + } + + ngOnDestroy() { + this.notifSub.unsubscribe(); + } + + /** + * @private + * + * Executes a notification action and updates the "expanded-notification" + * view if required. + * + * @param n The notification object. + * @param actionId The ID of the action to execute. + * @param event The mouse click event. + */ + execute(n: _Notification, action: Action, event: MouseEvent) { + event.preventDefault(); + event.stopPropagation(); + + this.notifsService.execute(n, action) + .subscribe() + } + + /** + * @private + * Toggles between list mode and notification-view mode. + * + * @param notif The notification that has been clicked. + */ + toggelView(notif: _Notification) { + const ref = this.dialog.create(NotificationComponent, { + backdrop: 'light', + autoclose: true, + data: notif, + }); + } +} diff --git a/desktop/angular/src/app/shared/notification/notification.html b/desktop/angular/src/app/shared/notification/notification.html new file mode 100644 index 00000000..c3d7bcf6 --- /dev/null +++ b/desktop/angular/src/app/shared/notification/notification.html @@ -0,0 +1,27 @@ +
+ Notification + + + Broadcast Notification + + + + + + + +

{{notification.Title}}

+ + + +
+ +
+
diff --git a/desktop/angular/src/app/shared/notification/notification.scss b/desktop/angular/src/app/shared/notification/notification.scss new file mode 100644 index 00000000..5142a8c0 --- /dev/null +++ b/desktop/angular/src/app/shared/notification/notification.scss @@ -0,0 +1,48 @@ +:host { + @apply block; + max-width: 24rem; +} + +caption { + @apply text-xxs; + opacity: .6; +} + +h1 { + @apply text-base font-normal my-4; +} + +.message, +h1 { + flex-shrink: 0; + text-overflow: ellipsis; + word-break: normal; +} + +.message { + flex-grow: 1; + padding: 0; +} + +.close-icon { + position: absolute; + top: 1rem; + right: 1rem; + opacity: .7; + cursor: pointer; + + &:hover { + opacity: 1; + } +} + +.buttons { + width: 100%; + display: flex; + + @apply flex flex-row justify-end gap-2; +} + +a { + text-decoration: underline; +} diff --git a/desktop/angular/src/app/shared/notification/notification.ts b/desktop/angular/src/app/shared/notification/notification.ts new file mode 100644 index 00000000..a50dcdee --- /dev/null +++ b/desktop/angular/src/app/shared/notification/notification.ts @@ -0,0 +1,65 @@ +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ChangeDetectionStrategy, Component, EventEmitter, HostBinding, Input, OnInit, Output, inject } from '@angular/core'; +import { SFNG_DIALOG_REF } from '@safing/ui'; +import { Action, NotificationState, NotificationsService, getNotificationTypeString } from '../../services'; +import { _Notification } from '../notification-list/notification-list.component'; + +@Component({ + selector: 'app-notification', + templateUrl: './notification.html', + styleUrls: ['./notification.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NotificationComponent implements OnInit { + readonly ref = inject(SFNG_DIALOG_REF); + readonly notification: _Notification = inject(SFNG_DIALOG_REF).data; + + /** + * The host tag of the notification component has the notification type + * and the notification state as a class name set. + * Examples: + * + * notif-action-required notif-prompt + */ + @HostBinding('class') + get hostClass(): string { + let cls = `notif-${this.state}`; + if (!!this.notification) { + cls = `${cls} notif-${getNotificationTypeString(this.notification.Type)}` + } + return cls + } + + state: NotificationState = NotificationState.Invalid; + + ngOnInit() { + if (!!this.notification) { + this.state = this.notification.State || NotificationState.Invalid; + } else { + this.state = NotificationState.Invalid; + } + } + + @Input() + set allowMarkdown(v: any) { + this._markdown = coerceBooleanProperty(v); + } + get allowMarkdown() { return this._markdown; } + private _markdown: boolean = true; + + @Output() + actionExecuted: EventEmitter = new EventEmitter(); + + constructor(private notifService: NotificationsService) { } + + execute(n: _Notification, action: Action) { + this.notifService.execute(n, action) + .subscribe( + () => { + this.actionExecuted.next(action) + this.ref.close(); + }, + err => console.error(err), + ) + } +} diff --git a/desktop/angular/src/app/shared/pipes/bytes.pipe.ts b/desktop/angular/src/app/shared/pipes/bytes.pipe.ts new file mode 100644 index 00000000..a0be5486 --- /dev/null +++ b/desktop/angular/src/app/shared/pipes/bytes.pipe.ts @@ -0,0 +1,28 @@ +import { DecimalPipe } from "@angular/common"; +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + pure: true, + name: 'bytes', +}) +export class BytesPipe implements PipeTransform { + transform(value: any, decimal: string = '1.0-2', ...args: any[]) { + value = +value; // convert to number + + const ceilings = [ + 'B', + 'kB', + 'MB', + 'GB', + 'TB' + ] + + let idx = 0; + while (value > 1024 && idx < ceilings.length - 1) { + value = value / 1024; + idx++ + } + + return (new DecimalPipe('en-US')).transform(value, decimal) + ' ' + ceilings[idx]; + } +} diff --git a/desktop/angular/src/app/shared/pipes/common-pipes.module.ts b/desktop/angular/src/app/shared/pipes/common-pipes.module.ts new file mode 100644 index 00000000..c64df8a1 --- /dev/null +++ b/desktop/angular/src/app/shared/pipes/common-pipes.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from "@angular/core"; +import { BytesPipe } from "./bytes.pipe"; +import { TimeAgoPipe } from "./time-ago.pipe"; +import { ToAppProfilePipe } from "./to-profile.pipe"; +import { DurationPipe } from "./duration.pipe"; +import { RoundPipe } from "./round.pipe"; +import { ToSecondsPipe } from "./to-seconds.pipe"; + +@NgModule({ + declarations: [ + TimeAgoPipe, + BytesPipe, + ToAppProfilePipe, + DurationPipe, + RoundPipe, + ToSecondsPipe + ], + exports: [ + TimeAgoPipe, + BytesPipe, + ToAppProfilePipe, + DurationPipe, + RoundPipe, + ToSecondsPipe + ] +}) +export class CommonPipesModule { } diff --git a/desktop/angular/src/app/shared/pipes/duration.pipe.ts b/desktop/angular/src/app/shared/pipes/duration.pipe.ts new file mode 100644 index 00000000..df33c283 --- /dev/null +++ b/desktop/angular/src/app/shared/pipes/duration.pipe.ts @@ -0,0 +1,103 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +const millisecond = 1; +const second = 1000 * millisecond; +const minute = 60 * second; +const hour = 60 * minute; +const day = 24 * hour; + +export function formatDuration(millis: number, skipDays = false, skipMillis = false): string { + const sign = millis < 0 ? '-' : ''; + let val = Math.abs(millis); + let str = ''; + + if (millis === 0) { + return '0'; + } + + if (!skipDays) { + const days = Math.floor(val / day) + if (days > 0) { + str += days.toString() + 'd '; + val -= days * day; + } + } + + const hours = Math.floor(val / hour); + if (hours > 0) { + str += hours.toString() + 'h '; + val -= hours * hour; + } + + const minutes = Math.floor(val / minute); + if (minutes > 0) { + str += minutes.toString() + 'm '; + val -= minutes * minute; + } + + const seconds = Math.floor(val / second); + if (seconds > 0) { + str += seconds.toString() + 's '; + val -= seconds * second; + } + + if (!skipMillis) { + const ms = Math.floor(val / millisecond) + if (ms > 0) { + str += ms.toString() + 'ms ' + val -= ms * millisecond + } + } + + if (str.endsWith("")) { + str = str.substring(0, str.length - 1) + } + + return sign + str; +} + +@Pipe({ + name: 'duration', + pure: true +}) +export class DurationPipe implements PipeTransform { + transform(value: number | [string, string] | [Date, Date] | [number, number], ...args: any[]) { + if (Array.isArray(value)) { + let firstNum: number; + let secondNum: number; + + let [first, second] = value; + if (first instanceof Date || typeof first === 'string') { + first = new Date(first) + firstNum = first.getTime() + } else { + firstNum = first + } + if (second instanceof Date || typeof second === 'string') { + second = new Date(second); + secondNum = second.getTime() + } else { + secondNum = second + } + + if (secondNum < firstNum) { + const t = firstNum; + firstNum = secondNum + secondNum = t + } + + value = secondNum - firstNum + } + + if (value < second) { + + } + + const result = formatDuration(value); + if (result === '0') { + return '< 1s' + } + + return result + } +} diff --git a/desktop/angular/src/app/shared/pipes/index.ts b/desktop/angular/src/app/shared/pipes/index.ts new file mode 100644 index 00000000..6eddfdd2 --- /dev/null +++ b/desktop/angular/src/app/shared/pipes/index.ts @@ -0,0 +1,6 @@ +export * from './common-pipes.module'; +export * from './time-ago.pipe'; +export * from './to-profile.pipe'; +export * from './duration.pipe'; +export * from './to-seconds.pipe'; +export * from './round.pipe'; diff --git a/desktop/angular/src/app/shared/pipes/round.pipe.ts b/desktop/angular/src/app/shared/pipes/round.pipe.ts new file mode 100644 index 00000000..104ca0ac --- /dev/null +++ b/desktop/angular/src/app/shared/pipes/round.pipe.ts @@ -0,0 +1,15 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'round', + pure: true, +}) +export class RoundPipe implements PipeTransform { + transform(value: number, roundBy: number) { + if (isNaN(value)) { + return NaN + } + + return Math.floor(value / roundBy) * roundBy + } +} diff --git a/desktop/angular/src/app/shared/pipes/time-ago.pipe.ts b/desktop/angular/src/app/shared/pipes/time-ago.pipe.ts new file mode 100644 index 00000000..25f53ac7 --- /dev/null +++ b/desktop/angular/src/app/shared/pipes/time-ago.pipe.ts @@ -0,0 +1,56 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'timeAgo', + pure: true +}) +export class TimeAgoPipe implements PipeTransform { + transform(value: number | Date | string, ticker?: any): string { + return timeAgo(value); + } +} + +export const timeCeilings = [ + { ceiling: 1, text: "" }, + { ceiling: 60, text: "sec" }, + { ceiling: 3600, text: "min" }, + { ceiling: 86400, text: "hour" }, + { ceiling: 2629744, text: "day" }, + { ceiling: 31556926, text: "month" }, + { ceiling: Infinity, text: "year" } +] + +export function timeAgo(value: number | Date | string) { + if (typeof value === 'string') { + value = new Date(value) + } + + if (value instanceof Date) { + value = value.valueOf() / 1000; + } + + let suffix = 'ago' + + let diffInSeconds = Math.floor(((new Date()).valueOf() - (value * 1000)) / 1000); + if (diffInSeconds < 0) { + diffInSeconds = diffInSeconds * -1; + suffix = '' + } + + for (let i = timeCeilings.length - 1; i >= 0; i--) { + const f = timeCeilings[i]; + let n = Math.floor(diffInSeconds / f.ceiling); + if (n > 0) { + if (i < 1) { + return `< 1 min ` + suffix; + } + let text = timeCeilings[i + 1].text; + if (n > 1) { + text += 's'; + } + return `${n} ${text} ` + suffix + } + } + + return "< 1 min" + suffix // actually just now (diffInSeconds == 0) +} diff --git a/desktop/angular/src/app/shared/pipes/to-profile.pipe.ts b/desktop/angular/src/app/shared/pipes/to-profile.pipe.ts new file mode 100644 index 00000000..217e3b35 --- /dev/null +++ b/desktop/angular/src/app/shared/pipes/to-profile.pipe.ts @@ -0,0 +1,35 @@ +import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform, inject } from "@angular/core"; +import { AppProfile, AppProfileService } from "@safing/portmaster-api"; +import { Subscription } from "rxjs"; + +@Pipe({ + name: 'toAppProfile', + pure: false +}) +export class ToAppProfilePipe implements PipeTransform, OnDestroy { + profileService = inject(AppProfileService); + cdr = inject(ChangeDetectorRef); + + private _lastProfile: AppProfile | null = null; + private _lastKey: string | null = null; + private _subscription = Subscription.EMPTY; + + transform(key: string): AppProfile | null { + if (key !== this._lastKey) { + this._lastKey = key; + + this._subscription.unsubscribe(); + this._subscription = this.profileService.watchAppProfile(key) + .subscribe(value => { + this._lastProfile = value; + this.cdr.markForCheck(); + }) + } + + return this._lastProfile || null; + } + + ngOnDestroy(): void { + this._subscription.unsubscribe(); + } +} diff --git a/desktop/angular/src/app/shared/pipes/to-seconds.pipe.ts b/desktop/angular/src/app/shared/pipes/to-seconds.pipe.ts new file mode 100644 index 00000000..166fbf17 --- /dev/null +++ b/desktop/angular/src/app/shared/pipes/to-seconds.pipe.ts @@ -0,0 +1,19 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: 'toSeconds', + pure: true, +}) +export class ToSecondsPipe implements PipeTransform { + transform(value: Date | string, ...args: any[]) { + if (value === null || value === undefined) { + return NaN + } + + if (typeof value === 'string') { + value = new Date(value); + } + + return Math.floor(value.getTime() / 1000) + } +} diff --git a/desktop/angular/src/app/shared/process-details-dialog/index.ts b/desktop/angular/src/app/shared/process-details-dialog/index.ts new file mode 100644 index 00000000..cceeb5f8 --- /dev/null +++ b/desktop/angular/src/app/shared/process-details-dialog/index.ts @@ -0,0 +1 @@ +export * from './process-details-dialog'; diff --git a/desktop/angular/src/app/shared/process-details-dialog/process-details-dialog.html b/desktop/angular/src/app/shared/process-details-dialog/process-details-dialog.html new file mode 100644 index 00000000..85cfa1bb --- /dev/null +++ b/desktop/angular/src/app/shared/process-details-dialog/process-details-dialog.html @@ -0,0 +1,131 @@ +

+ Process Details +

+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name +
+ + {{ process.Name }} +
+
User{{ process.UserName }} ({{ process.UserID + }})
Process ID{{ process.Pid }}
Process Group ID{{ process.Pgid }}
Parent Process ID{{ process.ParentPid }}
Path{{ process.Path }} ({{ process.MatchingPath + }}) + +
Executable Name{{ process.ExecName }}
Command Line{{ process.CmdLine }} + +
+
+ Tags + +
+
+ This process does not have any tags. +
    +
  • + {{ tag.Key }} + {{ tag.Value }} +
  • +
+
+
+
+ + +
+
+ This process does not have any environment variables. +
+ + + + + + + + + +
{{ env.key }}{{ env.value }} + +
+
+
+
+ +
+ +
\ No newline at end of file diff --git a/desktop/angular/src/app/shared/process-details-dialog/process-details-dialog.scss b/desktop/angular/src/app/shared/process-details-dialog/process-details-dialog.scss new file mode 100644 index 00000000..609e4e1f --- /dev/null +++ b/desktop/angular/src/app/shared/process-details-dialog/process-details-dialog.scss @@ -0,0 +1,32 @@ +:host { + @apply flex flex-col gap-4 max-w-2xl; + min-width: 500px; + width: 60vw; + + min-height: 500px; + height: 60vh; + max-height: 80vh; + overflow: hidden; +} + +table.custom { + @apply w-full overflow-hidden; + + th, + td { + @apply px-2 align-top py-2; + } + + th { + text-align: left; + @apply w-32 text-secondary; + } + + td { + @apply whitespace-normal break-all; + } + + td:last-of-type { + @apply p-0; + } +} diff --git a/desktop/angular/src/app/shared/process-details-dialog/process-details-dialog.ts b/desktop/angular/src/app/shared/process-details-dialog/process-details-dialog.ts new file mode 100644 index 00000000..1f0daa0c --- /dev/null +++ b/desktop/angular/src/app/shared/process-details-dialog/process-details-dialog.ts @@ -0,0 +1,102 @@ +import { KeyValue } from '@angular/common'; +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { AppProfile, AppProfileService, FingerpringOperation, Fingerprint, FingerprintType, PortapiService, Process } from '@safing/portmaster-api'; +import { SfngDialogRef, SfngDialogService, SFNG_DIALOG_REF } from '@safing/ui'; +import { EditProfileDialog } from '../edit-profile-dialog'; + +@Component({ + selector: 'app-process-details', + templateUrl: './process-details-dialog.html', + styleUrls: ['./process-details-dialog.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProcessDetailsDialogComponent { + process: (Process & { ID: string }); + + constructor( + @Inject(SFNG_DIALOG_REF) private dialogRef: SfngDialogRef, + private dialog: SfngDialogService, + private portapi: PortapiService, + private profileService: AppProfileService + ) { + this.process = { + ...this.dialogRef.data, + ID: this.dialogRef.data.PrimaryProfileID, + } + } + + close() { + this.dialogRef.close(); + } + + createProfileForPath() { + this.createProfileWithFingerprint({ + Type: FingerprintType.Path, + Key: '', + Value: this.process.MatchingPath || this.process.Path, + Operation: FingerpringOperation.Equal, + }) + } + + createProfileForCmdline() { + this.createProfileWithFingerprint({ + Type: FingerprintType.Cmdline, + Key: '', + Value: this.process.CmdLine, + Operation: FingerpringOperation.Equal, + }) + } + + createProfileForEnv(env: KeyValue) { + const fp: Fingerprint = { + Type: FingerprintType.Env, + Key: env.key, + Value: env.value, + Operation: FingerpringOperation.Equal, + } + + this.createProfileWithFingerprint(fp) + } + + openParent() { + if (!!this.process.ParentPid) { + this.portapi.get(`network:tree/${this.process.ParentPid}-${this.process.ParentCreatedAt}`) + .subscribe(process => { + this.process = { + ...process, + ID: process.PrimaryProfileID, + }; + }) + } + } + + openGroup() { + this.profileService.getProcessByPid(this.process.Pid) + .subscribe(result => { + if (!result) { + return; + } + + this.process = { + ...result, + ID: result.PrimaryProfileID + }; + }) + } + + private createProfileWithFingerprint(fp: Fingerprint) { + let profilePreset: Partial = { + Fingerprints: [ + fp + ] + }; + + this.dialog.create(EditProfileDialog, { + data: profilePreset, + backdrop: true, + autoclose: false, + }) + + this.dialogRef.close(); + } +} diff --git a/desktop/angular/src/app/shared/prompt-list/index.ts b/desktop/angular/src/app/shared/prompt-list/index.ts new file mode 100644 index 00000000..b76fae32 --- /dev/null +++ b/desktop/angular/src/app/shared/prompt-list/index.ts @@ -0,0 +1 @@ +export { PromptListComponent as PromptWidgetComponent } from './prompt-list.component'; diff --git a/desktop/angular/src/app/shared/prompt-list/prompt-list.component.html b/desktop/angular/src/app/shared/prompt-list/prompt-list.component.html new file mode 100644 index 00000000..4d32a21e --- /dev/null +++ b/desktop/angular/src/app/shared/prompt-list/prompt-list.component.html @@ -0,0 +1,68 @@ +
+
+
+ + {{ profile.Name }} + {{ profile.prompts.length }} + + + Per Connection + Allow All + Block All + + Default Action + Allow App + Block App + + Change Default + + + App Settings + + +
+ +
+
+
+
+ + + {{ prompt.EventData?.Entity?.IP || 'N/A' }} + + + {{prompt.subdomain}}.{{prompt.domain}} + + + + + + + +
+
+ {{ profile.prompts.length - profile.promptsLimited.length }} + more +
+ +
+ Show less +
+
+
+
+ +
+ + + + + No Prompts +
+
\ No newline at end of file diff --git a/desktop/angular/src/app/shared/prompt-list/prompt-list.component.scss b/desktop/angular/src/app/shared/prompt-list/prompt-list.component.scss new file mode 100644 index 00000000..90b34aad --- /dev/null +++ b/desktop/angular/src/app/shared/prompt-list/prompt-list.component.scss @@ -0,0 +1,204 @@ +:host { + overflow: hidden; + max-height: 50vh; + display: flex; + flex-direction: column; + min-height: 10rem; + @apply w-80; + @apply bg-gray-300; + + padding-top: 1px; + padding-bottom: 3px; +} + +app-icon { + --app-icon-size: 13px; +} + +.scrollable { + @apply p-0; +} + +.group { + @apply mb-3; + + .group-header { + @apply px-2; + display: flex; + align-items: center; + margin-left: 4px; + height: 2rem; + + .app-name { + flex-grow: 1; + font-size: 0.7rem; + font-weight: 500; + color: #cacaca; + } + + span.prompt-count { + @apply mr-1; + font-size: 0.6rem; + font-weight: 600; + color: #cacaca; + transform: scale(0.95); + user-select: none; + } + } +} + +app-menu-item.item-seperator { + @apply border-t; + @apply border-buttons-dark +} + +.no-prompts { + @apply text-tertiary flex-grow; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + user-select: none; +} + +.prompts { + display: flex; + + .border { + margin-left: calc(0.5rem + 9px); + width: 0.5rem; + border-left-width: 2px; + border-bottom-width: 2px; + border-color: #292929; + } + + .prompt-container, + .prompt, + .actions { + display: flex; + } + + .prompt-container { + flex-grow: 1; + flex-direction: column; + padding-left: 0.6rem; + padding-right: 0.5rem; + padding-top: 0.4rem; + padding-bottom: 1rem; + + .prompt { + padding-left: 0.75rem; + margin-bottom: 4px; + background-color: #292929; + height: auto; + border-radius: 2px; + align-items: center; + overflow: hidden; + position: relative; + + &:hover { + background-color: #303030; + + .actions { + animation: .07s slidein-left ease-in-out; + opacity: 1; + transition: all .05s ease-in-out; + } + } + + .entity { + flex-grow: 1; + word-break: break-all; + white-space: normal; + font-size: 0.7rem; + font-weight: 500; + padding-top: 0.6rem; + padding-bottom: 0.6rem; + padding-left: 2px; + padding-right: 9px; + color: #cacaca; + + .subdomain { + font-size: 0.7rem; + font-weight: 500; + color: #999999; + } + } + + .actions { + min-width: 5rem; + flex-wrap: wrap; + height: 100%; + opacity: 0; + transition: all .05s ease-in-out; + position: absolute; + right: 0; + background-color: #292929; + + button { + outline: none; + @apply bg-transparent; + font-size: 0.6rem; + background-color: #3a3a3a; + padding-left: 1.25rem; + padding-right: 1.25rem; + text-transform: capitalize; + border-radius: 0; + font-weight: 500; + outline: none; + color: hsla(0, 0%, 100%, 0.548); + + padding-left: 1.25rem; + padding-right: 1.25rem; + text-transform: capitalize; + border-radius: 0; + font-weight: 500; + outline: none; + color: hsla(0, 0%, 100%, 0.548); + + &:hover { + background-color: #363636; + color: #ffffff; + } + + &:last-of-type { + background: transparent; + color: hsla(0, 0%, 100%, 0.562); + @apply ml-1; + transition: all cubic-bezier(0.175, 0.885, 0.32, 1.275) .2s; + + &:hover { + color: #ffffff; + } + } + } + } + } + } + + .more-available { + position: relative; + top: 1.4rem; + margin-top: -1rem; + cursor: pointer; + font-size: 0.7rem; + font-weight: 500; + color: #999999; + user-select: none; + + &:hover { + color: #cacaca; + } + } +} + +@keyframes slidein-left { + 0% { + transform: translateX(100%); + } + + 100% { + transform: translateX(0); + } +} diff --git a/desktop/angular/src/app/shared/prompt-list/prompt-list.component.ts b/desktop/angular/src/app/shared/prompt-list/prompt-list.component.ts new file mode 100644 index 00000000..d6a3ca36 --- /dev/null +++ b/desktop/angular/src/app/shared/prompt-list/prompt-list.component.ts @@ -0,0 +1,236 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, OnDestroy, OnInit, TrackByFunction } from '@angular/core'; +import { AppProfile, AppProfileService, deepClone, setAppSetting } from '@safing/portmaster-api'; +import { combineLatest, forkJoin, Observable, Subscription } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; +import { Action, ConnectionPrompt, NotificationsService, NotificationType } from 'src/app/services'; +import { moveInOutAnimation, moveInOutListAnimation } from 'src/app/shared/animations'; +import { ParsedDomain, parseDomain } from 'src/app/shared/utils'; +import { ActionIndicatorService } from '../action-indicator'; + +// ExtendedConnectionPrompt extends the normal connection prompt +// with parsed domain information. +interface ExtendedConnectionPrompt extends ConnectionPrompt, ParsedDomain { } + +// ProfilePrompts extends an application profile with prompt +// information mainly used for paginagtion. +interface ProfilePrompts extends AppProfile { + promptsLimited: ExtendedConnectionPrompt[]; + prompts: ExtendedConnectionPrompt[]; + showAll: boolean; +} + +// Number of prompts to display per application profile +// before we start to paginate the list of prompts. +const PromptLimit = 3; + +@Component({ + selector: 'app-prompt-list', + templateUrl: './prompt-list.component.html', + styleUrls: [ + './prompt-list.component.scss' + ], + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + moveInOutAnimation, + moveInOutListAnimation + ] +}) +export class PromptListComponent implements OnInit, OnDestroy { + profiles: ProfilePrompts[] = []; + + /** + * @private + * Sets "empty" class on the host element if no prompts are displayed + */ + @HostBinding('class.empty') + get isEmpty() { + return this.profiles.length === 0; + } + + // Subscription to new prompts and profile updates. + private subscription = Subscription.EMPTY; + + constructor( + private changeDetectorRef: ChangeDetectorRef, + private profileService: AppProfileService, + public notifService: NotificationsService, + public uai: ActionIndicatorService + ) { } + + trackPrompts: TrackByFunction = this.notifService.trackBy; + + ngOnInit() { + // filter the stream of all notifications to only emit + // prompts that are used by the privacy filter (filter:prompt prefix). + const prompts$: Observable = this.notifService + .new$ + .pipe( + map(notifs => notifs.filter(notif => { + return notif.Type === NotificationType.Prompt && + notif.EventID.startsWith("filter:prompt"); + })), + ); + + // each time the notification list is emitted make sure we have an + // up-to-date copy of the linked application profile as well. + const profiles$ = prompts$ + .pipe( + switchMap(notifs => { + // collect all profile keys in a distict set so we don't load + // them more that once. + var profileKeys = new Set(); + notifs.forEach(n => profileKeys.add( + this.profileService.getKey(n.EventData!.Profile.Source, n.EventData!.Profile.ID) + )); + // load all of them in parallel + return forkJoin( + Array.from(profileKeys).map(key => this.profileService.getAppProfileFromKey(key)) + ) + }) + ); + + // subscribe to updates on the prompt list and the related profiles. + this.subscription = + combineLatest([ + prompts$, + profiles$, + ]).subscribe(([prompts, profiles]) => { + + let promptsByProfile = new Map(); + + // for each prompt, make an "extended" connection prompt by parsing the + // domain and index them by profile key + prompts.forEach(prompt => { + // prompts must have the connection data attached. If not, ignore it + // here. + if (!prompt.EventData) { + return; + } + + // get the list of prompts indexed by the profile ID. if this is + // the first prompt for that profile create a new array and place + // it at the index. + let entries = promptsByProfile.get(prompt.EventData.Profile.ID); + if (!entries) { + entries = []; + promptsByProfile.set(prompt.EventData.Profile.ID, entries); + } + + // Create an "extended" version of the prompt by parsing + // and assigning the domain and subdomain values. + let copy: ExtendedConnectionPrompt = { + ...prompt, + domain: null, + subdomain: null, + } + Object.assign(copy, parseDomain(prompt.EventData.Entity.Domain)) + entries.push(copy) + }); + + // Convert the list of application profiles into a set of ProfilePrompts + // objects that we can use to actually display the prompts with pagination + // applied. + this.profiles = profiles + .filter(profile => !!promptsByProfile.get(profile.ID)) + .map(profile => { + const prompts = promptsByProfile.get(profile.ID)!; + return { + ...profile, + showAll: prompts.length < PromptLimit, + promptsLimited: prompts.slice(0, PromptLimit), + prompts: prompts, + }; + }) + .sort((a, b) => { + if (a.ID > b.ID) { + return 1; + } + if (a.ID < b.ID) { + return -1; + } + return 0; + }); + + this.changeDetectorRef.markForCheck(); + }) + } + + allow(prompt: ConnectionPrompt) { + let allowActions = [ + 'allow-domain-all', + 'allow-serving-ip', + 'allow-ip', + ]; + + for (let i = 0; i < allowActions.length; i++) { + const action = prompt.AvailableActions.find(a => a.ID === allowActions[i]) + if (!!action) { + this.execute(prompt, action); + return; + } + } + } + + block(prompt: ConnectionPrompt) { + let permitActions = [ + 'block-domain-all', + 'block-serving-ip', + 'block-ip', + ]; + + for (let i = 0; i < permitActions.length; i++) { + const action = prompt.AvailableActions.find(a => a.ID === permitActions[i]) + if (!!action) { + this.execute(prompt, action); + return; + } + } + } + + changeDefault(profile: ProfilePrompts, newDefault: 'permit' | 'block') { + + this.profileService + .getAppProfile(profile.Source, profile.ID) + .pipe( + map(rawProfile => { + const copy = deepClone(rawProfile); + setAppSetting(copy.Config || {}, 'filter/defaultAction', newDefault) + + return copy + }), + switchMap(updatedProfile => this.profileService.saveProfile(updatedProfile)), + ) + .subscribe({ + error: (err) => { + this.uai.error('Failed to change App Settings', this.uai.getErrorMessage(err)); + } + }) + + + setAppSetting(profile.Config || {}, 'filter/defaultAction', newDefault) + } + + allowAll(profile: ProfilePrompts) { + profile.prompts.forEach(prompt => this.allow(prompt)); + } + + denyAll(profile: ProfilePrompts) { + profile.prompts.forEach(prompt => this.block(prompt)); + } + + execute(prompt: ConnectionPrompt, action: Action) { + this.notifService.execute(prompt, action) + .subscribe({ + error: console.error, + }); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + /** @private - {@link TrackByFunction} for profile prompts */ + trackProfile(_: number, p: ProfilePrompts) { + return p.ID; + } +} diff --git a/desktop/angular/src/app/shared/security-lock/index.ts b/desktop/angular/src/app/shared/security-lock/index.ts new file mode 100644 index 00000000..71561750 --- /dev/null +++ b/desktop/angular/src/app/shared/security-lock/index.ts @@ -0,0 +1 @@ +export * from './security-lock'; diff --git a/desktop/angular/src/app/shared/security-lock/security-lock.html b/desktop/angular/src/app/shared/security-lock/security-lock.html new file mode 100644 index 00000000..146fb34a --- /dev/null +++ b/desktop/angular/src/app/shared/security-lock/security-lock.html @@ -0,0 +1,25 @@ +
+ + + + + + + + + + +

{{lockLevel?.displayText}}

+ + See Notifications + +
+
diff --git a/desktop/angular/src/app/shared/security-lock/security-lock.scss b/desktop/angular/src/app/shared/security-lock/security-lock.scss new file mode 100644 index 00000000..12533ded --- /dev/null +++ b/desktop/angular/src/app/shared/security-lock/security-lock.scss @@ -0,0 +1,120 @@ +svg.shield { + width: 100%; + max-width: 7.25rem; + + transform: scale(0.95); + + path { + top: 0px; + left: 0px; + transform-origin: center center; + } + + .shield-one { + transform: scale(.62); + } + + .shield-two { + animation-delay: -1.2s; + opacity: .6; + transform: scale(.8); + } + + .shield-three { + animation-delay: -2.5s; + opacity: .4; + transform: scale(1); + } + + &.text-green-300 { + filter: saturate(1.4); + + .shield-one { + fill: var(--protection-ok-primary); + } + + + .shield-two { + fill: var(--protection-ok-secondary); + } + + .shield-three { + fill: var(--protection-ok-tertiary); + } + + .shield-warn, + .shield-fail { + display: none; + } + + .shield-ok { + stroke: var(--background); + fill: none; + transform: scale(.5); + } + } + + &.text-yellow-300 { + filter: saturate(1.3); + + .shield-one { + fill: var(--protection-warn-primary); + } + + .shield-three, + .shield-two { + //animation: shield-pulse 3s linear; + } + + .shield-two { + fill: var(--protection-warn-secondary); + } + + .shield-three { + fill: var(--protection-warn-tertiary); + } + + .shield-ok, + .shield-fail { + display: none; + } + + .shield-warn { + stroke: var(--background); + fill: none; + transform: scale(.5); + } + } + + &.text-red-300 { + filter: saturate(1.3); + + .shield-one { + fill: var(--protection-fail-primary); + } + + .shield-three, + .shield-two { + //animation: shield-pulse 3s linear reverse; + } + + .shield-two { + fill: var(--protection-fail-secondary); + } + + .shield-three { + fill: var(--protection-fail-tertiary); + } + + .shield-warn, + .shield-ok { + display: none; + } + + .shield-fail { + stroke: var(--background); + fill: none; + transform: scale(.45); + } + } +} diff --git a/desktop/angular/src/app/shared/security-lock/security-lock.ts b/desktop/angular/src/app/shared/security-lock/security-lock.ts new file mode 100644 index 00000000..7b6922c3 --- /dev/null +++ b/desktop/angular/src/app/shared/security-lock/security-lock.ts @@ -0,0 +1,97 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, Input, OnInit, inject } from "@angular/core"; +import { SecurityLevel } from "@safing/portmaster-api"; +import { combineLatest } from "rxjs"; +import { FailureStatus, StatusService, Subsystem } from "src/app/services"; +import { fadeInAnimation, fadeOutAnimation } from "../animations"; + +interface SecurityOption { + level: SecurityLevel; + displayText: string; + class: string; + subText?: string; +} + +@Component({ + selector: 'app-security-lock', + templateUrl: './security-lock.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./security-lock.scss'], + animations: [ + fadeInAnimation, + fadeOutAnimation + ] +}) +export class SecurityLockComponent implements OnInit { + private destroyRef = inject(DestroyRef); + + lockLevel: SecurityOption | null = null; + + /** The display mode for the security lock */ + @Input() + mode: 'small' | 'full' = 'full' + + constructor( + private statusService: StatusService, + private cdr: ChangeDetectorRef, + ) { } + + ngOnInit(): void { + combineLatest([ + this.statusService.status$, + this.statusService.watchSubsystems() + ]) + .subscribe(([status, subsystems]) => { + const activeLevel = status.ActiveSecurityLevel; + const suggestedLevel = status.ThreatMitigationLevel; + + // By default the lock is green and we are "Secure" + this.lockLevel = { + level: SecurityLevel.Normal, + class: 'text-green-300', + displayText: 'Secure', + } + + // Find the highest failure-status reported by any module + // of any subsystem. + const failureStatus = subsystems.reduce((value: FailureStatus, system: Subsystem) => { + if (system.FailureStatus != 0) { + console.log(system); + } + return system.FailureStatus > value + ? system.FailureStatus + : value; + }, FailureStatus.Operational) + + // update the failure level depending on the highest + // failure status. + switch (failureStatus) { + case FailureStatus.Warning: + this.lockLevel = { + level: SecurityLevel.High, + class: 'text-yellow-300', + displayText: 'Warning' + } + break; + case FailureStatus.Error: + this.lockLevel = { + level: SecurityLevel.Extreme, + class: 'text-red-300', + displayText: 'Insecure' + } + break; + } + + // if the auto-pilot would suggest a higher (mitigation) level + // we are always Insecure + if (activeLevel < suggestedLevel) { + this.lockLevel = { + level: SecurityLevel.High, + class: 'high', + displayText: 'Insecure' + } + } + + this.cdr.markForCheck(); + }); + } +} diff --git a/desktop/angular/src/app/shared/spn-account-details/index.ts b/desktop/angular/src/app/shared/spn-account-details/index.ts new file mode 100644 index 00000000..623342d5 --- /dev/null +++ b/desktop/angular/src/app/shared/spn-account-details/index.ts @@ -0,0 +1 @@ +export * from './spn-account-details'; diff --git a/desktop/angular/src/app/shared/spn-account-details/spn-account-details.html b/desktop/angular/src/app/shared/spn-account-details/spn-account-details.html new file mode 100644 index 00000000..3b594057 --- /dev/null +++ b/desktop/angular/src/app/shared/spn-account-details/spn-account-details.html @@ -0,0 +1,101 @@ + +

Account Details

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Your Package{{ currentUser.current_plan?.name }}
Access Until{{ currentUser.subscription.ends_at | date:'medium' }}
Your Subscription{{ currentUser.current_plan?.name }}
Status{{ currentUser.subscription.state }}
Next Payment Date + {{ currentUser.subscription.next_billing_date | date:'medium' }} + via + {{ currentUser.subscription.payment_provider }} +
Access Paid Until{{ currentUser.subscription.ends_at | date:'medium' }}
Username{{ currentUser.username }}
Device Name{{ currentUser.device?.name }}
Account State{{ currentUser.state }}
Features{{ currentUser.current_plan?.feature_ids?.join(", ") }}
Device ID{{currentUser.device?.id}}
Logged in Since{{ currentUser.LoggedInAt | date:'medium' }}
+ +
+ + +
+ + + Open Account Page + + + + +
+
+ + diff --git a/desktop/angular/src/app/shared/spn-account-details/spn-account-details.scss b/desktop/angular/src/app/shared/spn-account-details/spn-account-details.scss new file mode 100644 index 00000000..f1c7dc0d --- /dev/null +++ b/desktop/angular/src/app/shared/spn-account-details/spn-account-details.scss @@ -0,0 +1,7 @@ +table tr { + background-color: transparent !important; +} + +table .table-section-start { + border-top: 1.5rem solid transparent; +} diff --git a/desktop/angular/src/app/shared/spn-account-details/spn-account-details.ts b/desktop/angular/src/app/shared/spn-account-details/spn-account-details.ts new file mode 100644 index 00000000..512f6674 --- /dev/null +++ b/desktop/angular/src/app/shared/spn-account-details/spn-account-details.ts @@ -0,0 +1,83 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, Inject, OnInit, Optional, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { SPNService, UserProfile } from "@safing/portmaster-api"; +import { SFNG_DIALOG_REF, SfngDialogRef } from "@safing/ui"; +import { catchError, delay, of, tap } from "rxjs"; +import { ActionIndicatorService } from "../action-indicator"; + +@Component({ + templateUrl: './spn-account-details.html', + styleUrls: ['./spn-account-details.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SPNAccountDetailsComponent implements OnInit { + private destroyRef = inject(DestroyRef); + + /** Whether or not we're currently refreshing the user profile from the customer agent */ + refreshing = false; + + /** Whether or not we're still waiting for the user profile to be fetched from the backend */ + loadingProfile = true; + + currentUser: UserProfile | null = null; + + constructor( + private spnService: SPNService, + private cdr: ChangeDetectorRef, + private uai: ActionIndicatorService, + @Inject(SFNG_DIALOG_REF) @Optional() public dialogRef: SfngDialogRef, + ) { } + + /** + * Force a refresh of the local user account + * + * @private - template only + */ + refreshAccount() { + this.refreshing = true; + this.spnService.userProfile(true) + .pipe( + delay(1000), + tap(() => { + this.refreshing = false; + this.cdr.markForCheck(); + }), + ) + .subscribe() + } + + /** + * Logout of your safing account + * + * @private - template only + */ + logout() { + this.spnService.logout() + .pipe(tap(() => this.dialogRef?.close())) + .subscribe(this.uai.httpObserver('SPN Logout', 'SPN Logout')) + } + + ngOnInit(): void { + this.loadingProfile = false; + this.spnService.profile$ + .pipe( + takeUntilDestroyed(this.destroyRef), + catchError(err => of(null)), + ) + .subscribe({ + next: (profile) => { + this.loadingProfile = false; + this.currentUser = profile || null; + + this.cdr.markForCheck(); + }, + complete: () => { + // Database entry deletion will complete the observer. + this.loadingProfile = false; + this.currentUser = null; + + this.cdr.markForCheck(); + }, + }) + } +} diff --git a/desktop/angular/src/app/shared/spn-login/index.ts b/desktop/angular/src/app/shared/spn-login/index.ts new file mode 100644 index 00000000..25850856 --- /dev/null +++ b/desktop/angular/src/app/shared/spn-login/index.ts @@ -0,0 +1 @@ +export * from './spn-login'; diff --git a/desktop/angular/src/app/shared/spn-login/spn-login.html b/desktop/angular/src/app/shared/spn-login/spn-login.html new file mode 100644 index 00000000..8f59d86c --- /dev/null +++ b/desktop/angular/src/app/shared/spn-login/spn-login.html @@ -0,0 +1,70 @@ + +
+

+
+ + + + + + + + + + + + + + + + +
+ + Safing Account Login + + Unlock powerful features. + + +

+ + +
+ You have been logged out by the account server. +
+ Please check your account. +
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
diff --git a/desktop/angular/src/app/shared/spn-login/spn-login.scss b/desktop/angular/src/app/shared/spn-login/spn-login.scss new file mode 100644 index 00000000..232d51ee --- /dev/null +++ b/desktop/angular/src/app/shared/spn-login/spn-login.scss @@ -0,0 +1,53 @@ +:host { + display: block; + width: 100%; +} + +.custom-form-input { + background: none; + @apply border-0 border-b border-buttons-light text-secondary font-medium px-0; + + &:active, + &:focus { + background: none; + } +} + +.logo-image { + @apply w-16 absolute; +} + +svg.logo-image { + animation-timing-function: cubic-bezier(0.445, 0.05, 0.55, 0.95); +} + +.spin { + animation-name: spin; + animation-duration: 3500ms; + animation-iteration-count: infinite; + animation-timing-function: linear; +} + +.reverse { + animation-name: spin-reverse; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +@keyframes spin-reverse { + 0% { + transform: rotate(360deg); + } + + 100% { + transform: rotate(0deg); + } +} diff --git a/desktop/angular/src/app/shared/spn-login/spn-login.ts b/desktop/angular/src/app/shared/spn-login/spn-login.ts new file mode 100644 index 00000000..a5ae4172 --- /dev/null +++ b/desktop/angular/src/app/shared/spn-login/spn-login.ts @@ -0,0 +1,70 @@ +import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, Input, OnInit, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { SPNService, UserProfile } from "@safing/portmaster-api"; +import { catchError, finalize, of } from "rxjs"; +import { ActionIndicatorService } from "../action-indicator"; + +@Component({ + selector: 'app-spn-login', + templateUrl: './spn-login.html', + styleUrls: ['./spn-login.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SPNLoginComponent implements OnInit { + private destroyRef = inject(DestroyRef); + + /** The current user profile if the user is already logged in */ + profile: UserProfile | null = null; + + /** The value of the username text box */ + username: string = ''; + + /** The value of the password text box */ + password: string = ''; + + @Input() + set forcedLogout(v: any) { + this._forcedLogout = coerceBooleanProperty(v); + } + get forcedLogout() { return this._forcedLogout } + private _forcedLogout = false; + + constructor( + private spnService: SPNService, + private uai: ActionIndicatorService, + private cdr: ChangeDetectorRef + ) { } + + login(): void { + if (!this.username || !this.password) { + return; + } + + this.spnService.login({ + username: this.username, + password: this.password + }) + .pipe(finalize(() => { + this.password = ''; + })) + .subscribe(this.uai.httpObserver('SPN Login', 'SPN Login')) + } + + ngOnInit(): void { + this.spnService.profile$ + .pipe( + takeUntilDestroyed(this.destroyRef), + catchError(() => of(null)) + ) + .subscribe(profile => { + this.profile = profile || null; + + if (!!this.profile) { + this.username = this.profile.username; + } + + this.cdr.markForCheck(); + }); + } +} diff --git a/desktop/angular/src/app/shared/spn-network-status/index.ts b/desktop/angular/src/app/shared/spn-network-status/index.ts new file mode 100644 index 00000000..bfa12d5d --- /dev/null +++ b/desktop/angular/src/app/shared/spn-network-status/index.ts @@ -0,0 +1 @@ +export * from './spn-network-status'; diff --git a/desktop/angular/src/app/shared/spn-network-status/spn-network-status.html b/desktop/angular/src/app/shared/spn-network-status/spn-network-status.html new file mode 100644 index 00000000..83196071 --- /dev/null +++ b/desktop/angular/src/app/shared/spn-network-status/spn-network-status.html @@ -0,0 +1,28 @@ + +
+

Network Status

+ +
+ + Loading Network Status ... +
+
    +
  • +
    + {{ issue.title }} + {{ issue.closed ? 'closed' : 'opened'}} by {{ issue.user }} + {{ + issue.createdAt | timeAgo + }} +
    + +
    + + +
    +
  • +
+
diff --git a/desktop/angular/src/app/shared/spn-network-status/spn-network-status.scss b/desktop/angular/src/app/shared/spn-network-status/spn-network-status.scss new file mode 100644 index 00000000..6c73c8bb --- /dev/null +++ b/desktop/angular/src/app/shared/spn-network-status/spn-network-status.scss @@ -0,0 +1,71 @@ +:host { + @apply block; + min-width: 500px; + width: 50vw; +} + +.issue-list { + width: 100%; + + &, + ul { + overflow-y: auto; + } + + .issue { + position: relative; + display: flex; + flex-direction: column; + cursor: pointer; + @apply mx-2; + + .header { + @apply p-4; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + cursor: pointer; + } + + @apply rounded; + @apply bg-cards-primary; + + .title { + @apply mr-4; + } + + span { + word-break: keep-all; + } + + &:not(:last-child) { + margin-bottom: 0.5rem; + } + + .body { + @apply bg-cards-secondary; + @apply rounded-b; + @apply p-4; + } + + .meta { + @apply text-tertiary; + @apply font-normal; + opacity: .7; + font-size: 95%; + } + + &:hover { + @apply bg-cards-tertiary; + } + + fa-icon { + position: absolute; + right: 1rem; + top: 1rem; + opacity: .8; + cursor: pointer; + } + } +} diff --git a/desktop/angular/src/app/shared/spn-network-status/spn-network-status.ts b/desktop/angular/src/app/shared/spn-network-status/spn-network-status.ts new file mode 100644 index 00000000..131c907f --- /dev/null +++ b/desktop/angular/src/app/shared/spn-network-status/spn-network-status.ts @@ -0,0 +1,65 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, TrackByFunction, inject } from "@angular/core"; +import { map } from "rxjs"; +import { INTEGRATION_SERVICE } from "src/app/integration"; +import { Issue, SupportHubService } from "src/app/services"; + +/** The name of the SPN repository used to filter SPN support hub issues. */ +const SPNRepository = "spn"; + +/** A set of issue labels that are eligible to be displayed */ +const SPNTagSet = new Set(["network status"]) + +interface _Issue extends Issue { + expanded: boolean; +} + +@Component({ + selector: 'app-spn-network-status', + templateUrl: './spn-network-status.html', + styleUrls: ['./spn-network-status.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SPNNetworkStatusComponent implements OnInit { + private readonly integration = inject(INTEGRATION_SERVICE); + private readonly supportHub = inject(SupportHubService); + private readonly cdr = inject(ChangeDetectorRef); + + /** trackIssue is used as a track-by function when rendering SPN issues. */ + trackIssue: TrackByFunction<_Issue> = (_: number, issue: _Issue) => issue.url; + + spnIssues: _Issue[] = []; + + ngOnInit(): void { + this.supportHub.loadIssues() + .pipe( + map(issues => { + return issues + .filter(issue => issue.repository === SPNRepository && issue.labels?.some(l => { + return SPNTagSet.has(l); + })) + .reverse() + }) + ) + .subscribe(issues => { + let spnIssues: _Issue[] = issues + .map(i => { + const existing = this.spnIssues.find(existing => existing.url === i.url); + return { + ...i, + expanded: existing !== undefined ? existing.expanded : false + } + }) + this.spnIssues = spnIssues; + this.cdr.markForCheck(); + }) + } + + /** + * Open a github issue in a new tab/window + * + * @private - template only + */ + openIssue(issue: Issue) { + this.integration.openExternal(issue.url); + } +} diff --git a/desktop/angular/src/app/shared/spn-status/index.ts b/desktop/angular/src/app/shared/spn-status/index.ts new file mode 100644 index 00000000..a996c3cf --- /dev/null +++ b/desktop/angular/src/app/shared/spn-status/index.ts @@ -0,0 +1 @@ +export * from './spn-status'; diff --git a/desktop/angular/src/app/shared/spn-status/spn-status.html b/desktop/angular/src/app/shared/spn-status/spn-status.html new file mode 100644 index 00000000..84006d46 --- /dev/null +++ b/desktop/angular/src/app/shared/spn-status/spn-status.html @@ -0,0 +1,54 @@ +
+ + + +
+

SPN

+ + + + Increase privacy protection + + + Failed to connect + + + Connecting to the SPN ... + + + You're protected + + + + + Home: {{ spnStatus?.ConnectedIP }} via {{ spnStatus?.ConnectedTransport}} + + + + + +
+ +
+
+
+
+ Identities + {{ identities }} +
+
+
diff --git a/desktop/angular/src/app/shared/spn-status/spn-status.ts b/desktop/angular/src/app/shared/spn-status/spn-status.ts new file mode 100644 index 00000000..5cc26478 --- /dev/null +++ b/desktop/angular/src/app/shared/spn-status/spn-status.ts @@ -0,0 +1,128 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, OnInit, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute, Router } from '@angular/router'; +import { BoolSetting, ChartResult, ConfigService, FeatureID, Netquery, SPNService, SPNStatus, UserProfile } from "@safing/portmaster-api"; +import { SfngDialogService } from '@safing/ui'; +import { catchError, forkJoin, interval, of, startWith, switchMap } from "rxjs"; +import { fadeInAnimation, fadeOutAnimation } from "../animations"; +import { SPNAccountDetailsComponent } from '../spn-account-details'; + +@Component({ + selector: 'app-spn-status', + templateUrl: './spn-status.html', + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + fadeInAnimation, + fadeOutAnimation, + ] +}) +export class SPNStatusComponent implements OnInit { + private destroyRef = inject(DestroyRef); + + /** Whether or not the SPN is currently enabled */ + spnEnabled = false; + + /** The chart data for the SPN connection chart */ + spnConnChart: ChartResult[] = []; + + /** The current amount of SPN identities used */ + identities: number = 0; + + /** The current SPN user profile */ + profile: UserProfile | null = null; + + /** The current status of the SPN module */ + spnStatus: SPNStatus | null = null; + + /** Returns whether or not the current package has the SPN feature */ + get packageHasSPN() { + return this.profile?.current_plan?.feature_ids?.includes(FeatureID.SPN) + } + + constructor( + private configService: ConfigService, + private spnService: SPNService, + private netquery: Netquery, + private cdr: ChangeDetectorRef, + private router: Router, + private activeRoute: ActivatedRoute, + private dialog: SfngDialogService + ) { } + + ngOnInit(): void { + this.spnService + .profile$ + .pipe( + takeUntilDestroyed(this.destroyRef), + catchError(() => of(null)) + ) + .subscribe(profile => { + this.profile = profile || null; + + this.cdr.markForCheck(); + }); + + this.spnService.status$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(status => { + this.spnStatus = status; + + this.cdr.markForCheck(); + }) + + this.configService.watch("spn/enable") + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => { + this.spnEnabled = value; + + // If the user disabled the SPN clear the connection chart + // as well. + if (!this.spnEnabled) { + this.spnConnChart = []; + } + + this.cdr.markForCheck(); + }); + + interval(5000) + .pipe( + startWith(-1), + takeUntilDestroyed(this.destroyRef), + switchMap(() => forkJoin({ + chart: this.netquery.activeConnectionChart({ tunneled: { $eq: true } }), + identities: this.netquery.query({ + query: { tunneled: { $eq: true }, exit_node: { $ne: "" } }, + groupBy: ['exit_node'], + select: [ + 'exit_node', + { $count: { field: '*', as: 'totalCount' } } + ] + }, 'spn-status-get-connections-count-per-exit-node') + })) + ) + .subscribe(data => { + this.spnConnChart = data.chart; + this.identities = data.identities.length; + + this.cdr.markForCheck(); + }) + } + + openOrLogin() { + if (this.activeRoute.snapshot.firstChild?.url[0]?.path === "spn") { + this.dialog.create(SPNAccountDetailsComponent, { + autoclose: true, + backdrop: 'light' + }) + + return + } + + this.router.navigate(['/spn']) + } + + setSPNEnabled(v: boolean) { + this.configService.save(`spn/enable`, v) + .subscribe(); + } +} diff --git a/desktop/angular/src/app/shared/status-pilot/index.ts b/desktop/angular/src/app/shared/status-pilot/index.ts new file mode 100644 index 00000000..1ec75e5b --- /dev/null +++ b/desktop/angular/src/app/shared/status-pilot/index.ts @@ -0,0 +1 @@ +export { StatusPilotComponent as PilotWidgetComponent } from "./pilot-widget"; diff --git a/desktop/angular/src/app/shared/status-pilot/pilot-widget.html b/desktop/angular/src/app/shared/status-pilot/pilot-widget.html new file mode 100644 index 00000000..52e41fbb --- /dev/null +++ b/desktop/angular/src/app/shared/status-pilot/pilot-widget.html @@ -0,0 +1,57 @@ + + + +
+ {{ activeLevelText }} + + + + + +
+ + +
+
+ + + + + + Auto Detect + + + + + + Manual + + + +
+ + +
+
+ + {{opt.displayText}} + + + {{opt.subText || ''}} + + +
+
+
+
+
diff --git a/desktop/angular/src/app/shared/status-pilot/pilot-widget.scss b/desktop/angular/src/app/shared/status-pilot/pilot-widget.scss new file mode 100644 index 00000000..3f1bcae7 --- /dev/null +++ b/desktop/angular/src/app/shared/status-pilot/pilot-widget.scss @@ -0,0 +1,208 @@ +:host { + overflow: visible; + position: relative; + display: flex; + justify-content: space-between; + background: none; + user-select: none; + align-items: center; + justify-content: space-evenly; + flex-direction: column; + + + @keyframes shield-pulse { + 0% { + transform: scale(.62); + opacity: 1; + } + + 100% { + transform: scale(1.1); + opacity: 0; + } + } + + @keyframes pulse-opacity { + 0% { + opacity: 0.1; + } + + 100% { + opacity: 1; + } + } +} + +.spn-status { + background-color: var(--info-blue); + border-radius: 100%; + display: flex; + align-items: center; + justify-content: center; + opacity: 1 !important; + padding: 0.2rem; + transform: scale(0.8); + position: absolute; + bottom: 42px; + right: 18px; + + &.connected { + background-color: theme('colors.info.blue'); + } + + &.connecting, + &.failed { + background-color: theme('colors.info.gray'); + } + + svg { + stroke: white; + } +} + +::ng-deep { + + .network-rating-level-list { + @apply p-3 rounded; + + flex-grow: 1; + + label { + opacity: 0.6; + font-size: 0.75rem; + font-weight: 500; + } + + div.rate-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 0 0.3rem 0; + margin-right: 0.11rem; + + .auto-detect { + height: 5px; + width: 5px; + margin-right: 10px; + margin-bottom: 1px; + background-color: #4995f3; + border-radius: 50%; + display: inline-block; + } + } + + &:not(.auto-pilot) { + div.level.selected { + div { + background-color: #292929; + } + + &:after { + transition: none; + opacity: 0 !important; + } + } + } + + div.level { + position: relative; + padding: 2px; + margin-top: 0.155rem; + cursor: pointer; + overflow: hidden; + z-index: 1; + + fa-icon[icon*="question-circle"] { + float: right; + } + + &:after { + transition: all cubic-bezier(0.19, 1, 0.82, 1) .2s; + @apply rounded; + content: ""; + filter: saturate(1.3); + background-image: linear-gradient(90deg, #226ab79f 0%, rgba(2, 0, 36, 0) 45%); + transform: translateX(100%); + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -1; + opacity: 0; + } + + div { + background-color: #202020; + border-radius: 2px; + padding: 9px 17px 10px 18px; + display: block; + opacity: 0.55; + + span { + font-size: 0.725rem; + font-weight: 400; + } + + .situation { + @apply text-tertiary; + @apply ml-2; + font-size: 0.6rem; + font-weight: 600; + } + + svg.help { + width: 0.95rem; + float: right; + padding: 0; + margin: 0; + margin-top: 1.5px; + + .inner { + stroke: var(--text-secondary); + } + + &:hover, + &:active { + .inner { + stroke: var(--text-primary); + } + } + } + } + + &.selected { + div { + background-color: #292929; + opacity: 1; + } + } + + &.selected, + &.suggested { + &:after { + transform: translateX(0%); + opacity: 1; + } + + } + + &.suggested { + &:after { + animation: pulse-opacity 1s ease-in-out infinite alternate; + } + } + + &:hover, + &:active { + div { + opacity: 1; + + span { + opacity: 1; + } + } + } + } + } +} diff --git a/desktop/angular/src/app/shared/status-pilot/pilot-widget.ts b/desktop/angular/src/app/shared/status-pilot/pilot-widget.ts new file mode 100644 index 00000000..4fa01dd6 --- /dev/null +++ b/desktop/angular/src/app/shared/status-pilot/pilot-widget.ts @@ -0,0 +1,115 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { ConfigService, SecurityLevel } from '@safing/portmaster-api'; +import { combineLatest } from 'rxjs'; +import { FailureStatus, StatusService, Subsystem } from 'src/app/services'; + +interface SecurityOption { + level: SecurityLevel; + displayText: string; + class: string; + subText?: string; +} + +@Component({ + selector: 'app-status-pilot', + templateUrl: './pilot-widget.html', + styleUrls: [ + './pilot-widget.scss' + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class StatusPilotComponent implements OnInit { + activeLevel: SecurityLevel = SecurityLevel.Off; + selectedLevel: SecurityLevel = SecurityLevel.Off; + suggestedLevel: SecurityLevel = SecurityLevel.Off; + activeOption: SecurityOption | null = null; + selectedOption: SecurityOption | null = null; + + mode: 'auto' | 'manual' = 'auto'; + + get activeLevelText() { + return this.options.find(opt => opt.level === this.activeLevel)?.displayText || ''; + } + + readonly options: SecurityOption[] = [ + { + level: SecurityLevel.Normal, + displayText: 'Trusted', + class: 'low', + subText: 'Home Network' + }, + { + level: SecurityLevel.High, + displayText: 'Untrusted', + class: 'medium', + subText: 'Public Network' + }, + { + level: SecurityLevel.Extreme, + displayText: 'Danger', + class: 'high', + subText: 'Hacked Network' + }, + ]; + + get networkRatingEnabled$() { return this.configService.networkRatingEnabled$ } + + constructor( + private statusService: StatusService, + private changeDetectorRef: ChangeDetectorRef, + private configService: ConfigService, + ) { } + + ngOnInit() { + + combineLatest([ + this.statusService.status$, + this.statusService.watchSubsystems() + ]) + .subscribe(([status, subsystems]) => { + this.activeLevel = status.ActiveSecurityLevel; + this.selectedLevel = status.SelectedSecurityLevel; + this.suggestedLevel = status.ThreatMitigationLevel; + + if (this.selectedLevel === SecurityLevel.Off) { + this.mode = 'auto'; + } else { + this.mode = 'manual'; + } + + this.selectedOption = this.options.find(opt => opt.level === this.selectedLevel) || null; + this.activeOption = this.options.find(opt => opt.level === this.activeLevel) || null; + + // Find the highest failure-status reported by any module + // of any subsystem. + const failureStatus = subsystems.reduce((value: FailureStatus, system: Subsystem) => { + if (system.FailureStatus != 0) { + console.log(system); + } + return system.FailureStatus > value + ? system.FailureStatus + : value; + }, FailureStatus.Operational) + + this.changeDetectorRef.markForCheck(); + }); + } + + updateMode(mode: 'auto' | 'manual') { + this.mode = mode; + + if (mode === 'auto') { + this.selectLevel(SecurityLevel.Off); + } else { + this.selectLevel(this.activeLevel); + } + } + + selectLevel(level: SecurityLevel) { + if (this.mode === 'auto' && level !== SecurityLevel.Off) { + this.mode = 'manual'; + } + + this.statusService.selectLevel(level).subscribe(); + } +} diff --git a/desktop/angular/src/app/shared/text-placeholder/index.ts b/desktop/angular/src/app/shared/text-placeholder/index.ts new file mode 100644 index 00000000..8d04c94a --- /dev/null +++ b/desktop/angular/src/app/shared/text-placeholder/index.ts @@ -0,0 +1 @@ +export { PlaceholderComponent } from './placeholder'; diff --git a/desktop/angular/src/app/shared/text-placeholder/placeholder.scss b/desktop/angular/src/app/shared/text-placeholder/placeholder.scss new file mode 100644 index 00000000..88140deb --- /dev/null +++ b/desktop/angular/src/app/shared/text-placeholder/placeholder.scss @@ -0,0 +1,32 @@ +.text-placeholder { + display : inline-block; + height : 0.75rem; + position: relative; + + .background { + @apply rounded; + opacity : 0.8; + animation-duration : 6s; + animation-fill-mode : forwards; + animation-iteration-count: infinite; + animation-name : placeHolderShimmer; + animation-timing-function: linear; + background : linear-gradient(to right, #4b4b4b 8%, #5a5a5a 18%, #4b4b4b 33%); + position : absolute; + backface-visibility : hidden; + left : 0; + right : 0; + top : 2px; + bottom : 0; + } +} + +@keyframes placeHolderShimmer { + 0% { + background-position: 0px 0; + } + + 100% { + background-position: 100em 0; + } +} diff --git a/desktop/angular/src/app/shared/text-placeholder/placeholder.ts b/desktop/angular/src/app/shared/text-placeholder/placeholder.ts new file mode 100644 index 00000000..0b9797a3 --- /dev/null +++ b/desktop/angular/src/app/shared/text-placeholder/placeholder.ts @@ -0,0 +1,61 @@ +import { AfterContentChecked, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input } from '@angular/core'; + +@Component({ + selector: 'app-text-placeholder', + template: ` + +
+
+ + `, + styleUrls: ['./placeholder.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PlaceholderComponent implements AfterContentChecked { + @Input() + set width(v: string | number) { + if (typeof v === 'number') { + this._width = `${v}px`; + return + } + + switch (v) { + case 'small': + this._width = '5rem'; + break; + case 'medium': + this._width = '10rem'; + break; + case 'large': + this._width = '15rem'; + break + default: + this._width = v; + } + } + get width() { return this._width; } + private _width: string = '10rem'; + + @Input() + mode: 'auto' | 'input' = 'auto'; + + @Input() + loading = true; + + constructor( + private elementRef: ElementRef, + private changeDetector: ChangeDetectorRef, + ) { } + + ngAfterContentChecked() { + if (this.mode === 'input') { + return; + } + + const show = this.elementRef.nativeElement.innerText === ''; + if (this.loading != show) { + this.loading = show; + this.changeDetector.detectChanges(); + } + } +} diff --git a/desktop/angular/src/app/shared/utils.ts b/desktop/angular/src/app/shared/utils.ts new file mode 100644 index 00000000..c36caa07 --- /dev/null +++ b/desktop/angular/src/app/shared/utils.ts @@ -0,0 +1,76 @@ +import { parse } from 'psl'; + +export interface ParsedDomain { + domain: string | null; + subdomain: string | null; +} +export function parseDomain(scope: string): ParsedDomain { + // Due to https://github.com/lupomontero/psl/issues/185 + // parse will throw an error for service-discovery lookups + // so make sure we split them apart. + const domainParts = scope.split(".") + const lastUnderscorePart = domainParts.length - [...domainParts].reverse().findIndex(dom => dom.startsWith("_")) + let result: ParsedDomain = { + domain: null, + subdomain: null, + } + + let cleanedDomain = scope; + let removedPrefix = ''; + if (lastUnderscorePart <= domainParts.length) { + removedPrefix = domainParts.slice(0, lastUnderscorePart).join('.') + cleanedDomain = domainParts.slice(lastUnderscorePart).join('.') + } + + const parsed = parse(cleanedDomain); + if ('listed' in parsed) { + result.domain = parsed.domain || scope; + result.subdomain = removedPrefix; + if (!!parsed.subdomain) { + if (removedPrefix != '') { + result.subdomain += '.'; + } + result.subdomain += parsed.subdomain; + } + } + + return result +} + +export function binarySearch(array: T[], what: T, sortFunc: (a: T, b: T) => number): number { + let l = 0; + let h = array.length - 1; + let currentIndex: number = 0; + + while (l <= h) { + currentIndex = (l + h) >>> 1; + const result = sortFunc(what, array[currentIndex]); + if (result < 0) { + l = currentIndex + 1; + } else if (result > 0) { + h = currentIndex - 1; + } else { + return currentIndex; + } + } + return ~currentIndex; +} + +export function binaryInsert(array: T[], what: T, sortFunc: (a: T, b: T) => number, duplicate = false): number { + let idx = binarySearch(array, what, sortFunc); + if (idx >= 0) { + if (!duplicate) { + return idx; + } + } else { + // if `what` is not part of `array` than index is the bitwise complement + // of the expected index in array. + idx = ~idx; + } + array.splice(idx, 0, what) + return idx; +} + +export function objKeys(obj: T): (keyof T)[] { + return Object.keys(obj) as any; +} diff --git a/desktop/angular/src/assets b/desktop/angular/src/assets new file mode 120000 index 00000000..ec2e4be2 --- /dev/null +++ b/desktop/angular/src/assets @@ -0,0 +1 @@ +../assets \ No newline at end of file diff --git a/desktop/angular/src/electron-app.d.ts b/desktop/angular/src/electron-app.d.ts new file mode 100644 index 00000000..febea42b --- /dev/null +++ b/desktop/angular/src/electron-app.d.ts @@ -0,0 +1,41 @@ +declare global { + interface Window { + app: AppAPI; + } +} + +export class AppAPI { + /** Returns the current platform */ + getPlatform(): Promise; + + /** The installation directory of portmaster. */ + getInstallDir(): Promise; + + /** + * Open an URL or path using an external application. + * + * @param pathOrUrl The path or URL to open. + */ + openExternal(pathOrUrl: string): Promise; + + /** + * Creates a new URL with the file:// scheme. Works + * on any platform. + * + * @param path The path for the file URL. + */ + createFileURL(path: string): Promise; + + /** + * Returns a dataURL for the icon that is used to represent + * the path on this platform. + * This method only works on windows for now. On all other + * platforms an empty string is returned. + * + * @param path The path the the binary + */ + getFileIcon(path: string): Promise; + + /** Exit the electron appliction. */ + exitApp(): Promise; +} diff --git a/desktop/angular/src/environments/environment.prod.ts b/desktop/angular/src/environments/environment.prod.ts new file mode 100644 index 00000000..71b53cec --- /dev/null +++ b/desktop/angular/src/environments/environment.prod.ts @@ -0,0 +1,22 @@ +/* +export const environment = new class { + readonly supportHub = "https://support.safing.io" + readonly production = true; + + get httpAPI() { + return `http://${window.location.host}/api` + } + + get portAPI() { + const result = `ws://${window.location.host}/api/database/v1`; + return result; + } +} +*/ + +export const environment = { + production: false, + portAPI: "ws://127.0.0.1:817/api/database/v1", + httpAPI: "http://127.0.0.1:817/api", + supportHub: "https://support.safing.io" +}; diff --git a/desktop/angular/src/environments/environment.ts b/desktop/angular/src/environments/environment.ts new file mode 100644 index 00000000..5ef6df25 --- /dev/null +++ b/desktop/angular/src/environments/environment.ts @@ -0,0 +1,19 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false, + portAPI: "ws://127.0.0.1:817/api/database/v1", + httpAPI: "http://127.0.0.1:817/api", + supportHub: "https://support.safing.io" +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git a/desktop/angular/src/i18n/helptexts.yaml b/desktop/angular/src/i18n/helptexts.yaml new file mode 100644 index 00000000..e00b6023 --- /dev/null +++ b/desktop/angular/src/i18n/helptexts.yaml @@ -0,0 +1,370 @@ +########### +### Example + +myKey: + title: Tipup Example + content: | + This is the Markdown formatted content. + + This is a super cool, new feature that you will love! + It even supports markdown features like: + - order lists + - with multiple items + + And :rocket: emojis + + ### :tada: :facepalm: + url: https://docs.safing.io/?source=Portmaster + urlText: Show me! + nextKey: navMonitor + +############## +### Navigation + +introTipup: + title: Hey there! + content: | + Thanks for installing the Portmaster. + +intro: + title: Portmaster Tips + content: | + Open tips to learn how the Portmaster work. + + Tips like this one are found throughout the Portmaster. With some tips you can tour an element or a feature, like this: + nextKey: navShield + +navShield: + title: Status Shield & Dashboard + content: | + The shield gives you a high level overview of Portmaster's status. If turns any other color than green, look for a notification that tells you what is going on. + + __Click the shield in order to open the dashboard.__ + nextKey: navMonitor + +navMonitor: + title: Network Monitor + content: | + Oversee and investigate everything happening on your device. + nextKey: navApps + buttons: + - name: Take the tour + action: + Type: open-page + Payload: monitor + nextKey: networkMonitor + +navApps: + title: Per-App Settings + content: | + Configure per-app settings which override the global default. + nextKey: navMap + buttons: + - name: Take the tour + action: + Type: open-page + Payload: apps + nextKey: appsTitle + +navMap: + title: SPN Map + content: | + View the SPN map and see how your connections are routed. + nextKey: navSettings + +navSettings: + title: Global Settings + content: | + Configure global Portmaster settings. + nextKey: navSupport + buttons: + - name: Take the tour + action: + Type: open-page + Payload: settings + nextKey: globalSettings + +navSupport: + title: Get Help + content: | + Report a bug, contact support or view the extended Portmaster docs. + nextKey: navTools + buttons: + - name: Open Page + action: + Type: open-page + Payload: support + +navTools: + title: Version and Tools + content: | + View the Portmaster's version and use special actions and tools. + nextKey: navPower + +navPower: + title: Shutdown and Restart + content: | + Shutdown or Restart Portmaster. + nextKey: uiMode + +uiMode: + title: UI Mode + content: | + Quickly change the amount of settings and information shown. + + Hidden settings are still in effect. After closing the User Interface it changes back to the default. + buttons: + - name: Change Default UI Mode + action: + Type: "open-setting" + Payload: + Key: "core/expertiseLevel" + +############ +### Sidedash + +pilot-widget: + title: Portmaster Status + content: | + This shield shows you the current state of the Portmaster: + + - 🟢 all is well + - 🟡 something is off, please investigate + - 🔴 dangerous condition, respond immediately + + This color code is also displayed as part of the icon in the system tray. + +pilot-widget-NetworkRating: + title: Network Rating + content: | + Control your privacy even when connecting to new networks. + + In the Portmaster you configure settings to be active in one environment but not in the other, like allowing sensitive connections at home but not at the public library. + + The only thing you have to do is to change the network rating whenever you connect to a different network. + nextKey: pilot-widget-NetworkRating-Trusted + +pilot-widget-NetworkRating-Trusted: + title: "Network Rating: Trusted" + content: | + You trust the current network to be secure and protect you. + + Examples: + - your home network + - network of a trusted friends + nextKey: pilot-widget-NetworkRating-Untrusted + +pilot-widget-NetworkRating-Untrusted: + title: "Network Rating: Untrusted" + content: | + You do not trust the current network and question if it will keep you secure and private. + + Examples: + - public WiFi of a coffeeshop, a library, a train, a hotel, ... + - network of a non-tech-savvy relative + nextKey: pilot-widget-NetworkRating-Danger + +pilot-widget-NetworkRating-Danger: + title: "Network Rating: Danger" + content: | + You think that the current network is hacked or otherwise hostile towards you. + + Examples: + - something suspicious is going on in your home network + + _Note: In the "Danger" rating the Portmaster will become very protective. This might break functionality of apps or render them useless._ + +broadcast-info: + title: Broadcast Notifications + content: | + Broadcast Notifications are public messages downloaded by the Portmaster when checking for updates. + + The Portmaster then locally decides which messages should be displayed. + url: https://github.com/safing/portmaster/issues/703 + urlText: Learn More + +# TODO +# prompt-widget: +# title: Prompts +# content: | +# This is where you can more easily control the +# connections for the specific app for the time being. + +# How to use? In App settings, search for "Default Action" +# and set it to "Prompt". + +# Note: Don't set the "Prompt" setting in your browser, +# you will be spammed. You have been warned. +# nextKey: notification-widget + +# TODO +# notification-widget: +# title: Notifications +# content: | +# This informs you with what's going on with portmaster. +# Ie, Updates, Errors, Warring etc + +############# +### Dashboard + +dashboardIntro: + title: Dashboard + content: | + The Dashboard gives you a first overview of Portmaster's active features and what is happening on your device. + + Unless noted otherwise, all graphs and statistics shown are based on what Portmaster has seen in the last 10 minutes and are refreshed every 10 seconds. + +######################## +### Network Monitor Page + +networkMonitor: + title: Network Activity + content: | + Oversee everything happening on your device. + + Look at all network connections of all applications and processes that were active in the last 10 minutes. Click on any app or process to investigate further. + +# TODO: Wait for overview to be more useful. +# networkMonitor-Overview: +# title: Monitor Overview +# content: | +# This is just a placeholder for the meantime, but this is +# just the Network Monitor with 3 stats on it. + +# TODO: Wait for revamp of status indication. +# networkMonitor-App: +# title: App Activity +# content: | +# There are 3 colours. Ie, Green, Red, Gray. + +# Allowed(Green) +# The colour green shows that all the connections are allowed in +# the app. + +# Blocked(Red) +# The colour red shows that all the connections are blocked in +# the app. + +# Allowed/Blocked(Gray) +# The colour gray shows that some connections are +# allowed and blocked in the app. + +networkMonitor-App-Focus-connection-history: + title: Network Activity + content: | + Monitor connections as they happen. Click on any connection to view details and to take action. +

+ + + 2k+ + + + + + Status Summary +

+ Grouped connections have a colored bar showing the total amount of connections, + as well as the percentage between allowed (green) and blocked/failed connections (grey). +

+ An individual connection has three states:
+ Allowed
+ Blocked
+ Failed
+ + If the circle is full, your _current_ settings allowed or blocked the connection.
+ If the circle is empty, _previous_ settings allowed or blocked the connection. + Your current settings could decide differently. + +######################## +### Global Settings Page + +globalSettings: + title: Global Settings + content: | + Here you can set system-wide preferences and configure default rules for all your apps and connections. + + It is easy to create a stricter global ruleset and then create exceptions in the app settings, which override the global default. + +######################### +### Per-App Settings Page + +appsTitle: + title: Application Overview + content: | + All applications or processes that the Portmaster saw being active on the network are listed and can be configured here. + + Apps are categorized and only appear once: + + - **Active:** apps that are currently active and visible in the Network Monitor + - **Recently Used:** apps that were active some time within the last week + - **Recently Edited:** apps whose settings were edited within the last week + - **Other:** all other apps + +appSettings: + title: App Settings + content: | + Here you can configure app-specific settings which override the global settings. + + It is easy to create a stricter global ruleset and then create exceptions in the app settings, which override the global default. + nextKey: appSettings-Filter + +appSettings-Filter: + title: Display Mode + content: | + Quickly change what settings are displayed: + + **View Active:**
+ Only show app-specific settings which override the global default. + + **View All:**
+ Show all settings. App-specific settings which override the global default are highlighted. + +appSettings-QuickSettings: + title: Quickly Change the Most Important Settings + content: | + __Block Connections__ + + Set the default action for when nothing else allows or blocks an outgoing connection. + + When other settings might overwrite this, a yellow dot next to the toggle will inform you of possible exceptions. + + __SPN__ + + Quickly enable or disable SPN for this app. + + When other settings might overwrite this, a yellow dot next to the toggle will inform you of possible exceptions. + + __Keep History__ + + Save connections in a database (on disk) in order to view and search them later. + + Changes might take a couple minutes to apply to all connections. + +######################### +### Support Page + +support-page-related-issues: + title: Local Issue Search + content: | + Public issues are only searched for locally so no data leaves your device until you decide so. + + The public GitHub issues are downloaded via our support system to prevent exposure to GitHub. + +######################### +### Configuration Options + +spn: + title: Safing Privacy Network + content: | + The Safing Privacy Network (SPN) is a Portmaster Add-On that protects your identity + and Internet traffic from prying eyes. It spreads your connections over multiple server, + letting you access the Internet from many places at once in order to effectively hide + your tracks. + url: https://safing.io/spn/?source=Portmaster + urlText: Learn More + +########################### +# Process Matching and Fingerprints +process-tags: + title: Process Tags + content: Tags holds special metadata of processes and are gathered by Portmaster. You can use these tags in fingerprints to better match processes, which would otherwise be a lot more difficult or impossible to match correctly. diff --git a/desktop/angular/src/i18n/helptexts.yaml.d.ts b/desktop/angular/src/i18n/helptexts.yaml.d.ts new file mode 100644 index 00000000..979498e3 --- /dev/null +++ b/desktop/angular/src/i18n/helptexts.yaml.d.ts @@ -0,0 +1,24 @@ + +declare module 'js-yaml-loader!*' { + import { Action } from "src/app/services/notifications.types"; + export interface Button { + name: string; + action: Action; + nextKey?: string; + } + + export interface TipUp { + title: string; + content: string; + url?: string; + urlText?: string; + buttons?: Button[]; + nextKey?: string; + } + export interface HelpTexts { + [key: string]: TipUp; + } + + const content: HelpTexts; + export default content; +} diff --git a/desktop/angular/src/index.html b/desktop/angular/src/index.html new file mode 100644 index 00000000..8912951b --- /dev/null +++ b/desktop/angular/src/index.html @@ -0,0 +1,34 @@ + + + + + + Portmaster + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/angular/src/main.ts b/desktop/angular/src/main.ts new file mode 100644 index 00000000..2b10a238 --- /dev/null +++ b/desktop/angular/src/main.ts @@ -0,0 +1,94 @@ +import { enableProdMode, importProvidersFrom } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; +import { INTEGRATION_SERVICE, integrationServiceFactory } from './app/integration'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { PromptWidgetComponent } from './app/shared/prompt-list'; +import { PromptEntryPointComponent } from './app/prompt-entrypoint/prompt-entrypoint'; +import { provideHttpClient } from '@angular/common/http'; +import { provideRouter } from '@angular/router'; +import { PortmasterAPIModule } from '@safing/portmaster-api'; +import { NotificationsService } from './app/services'; +import { TauriIntegrationService } from './app/integration/taur-app'; + +if (environment.production) { + enableProdMode(); +} + +if (typeof (CSS as any)['registerProperty'] === 'function') { + (CSS as any).registerProperty({ + name: '--lock-color', + syntax: '*', + inherits: true, + initialValue: '10, 10, 10' + }) +} + +function handleExternalResources(e: Event) { + // get click target + let target: HTMLElement | null = e.target as HTMLElement; + // traverse until we reach an a tag + while (!!target && target.tagName !== "A") { + target = target.parentElement; + } + + if (!!target) { + let href = target.getAttribute("href"); + if (href?.startsWith("blob")) { + return + } + + if (!!href && !href.includes(location.hostname)) { + e.preventDefault(); + + integrationServiceFactory().openExternal(href); + } + } +} + +if (document.addEventListener) { + document.addEventListener("click", handleExternalResources); +} + +// load the font file but make sure to use the slimfix version +// windows. +{ + // we cannot use document.writeXX here as it's not allowed to + // write to Document from an async loaded script. + + let linkTag = document.createElement("link"); + linkTag.rel = "stylesheet"; + linkTag.href = "/assets/vendor/fonts/roboto.css"; + if (navigator.platform.startsWith("Win")) { + linkTag.href = "/assets/vendor/fonts/roboto-slimfix.css" + } + + document.head.appendChild(linkTag); +} + + +if (location.pathname !== "/prompt") { + // bootstrap our normal application + platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); + +} else { + // bootstrap the prompt interface + bootstrapApplication(PromptEntryPointComponent, { + providers: [ + provideHttpClient(), + importProvidersFrom(PortmasterAPIModule.forRoot({ + websocketAPI: "ws://localhost:817/api/database/v1", + httpAPI: "http://localhost:817/api" + })), + NotificationsService, + { + provide: INTEGRATION_SERVICE, + useClass: TauriIntegrationService + } + ], + }) +} + diff --git a/desktop/angular/src/polyfills.ts b/desktop/angular/src/polyfills.ts new file mode 100644 index 00000000..576bf9d7 --- /dev/null +++ b/desktop/angular/src/polyfills.ts @@ -0,0 +1,57 @@ +/*************************************************************************************************** + * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. + */ +import '@angular/localize/init'; +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/desktop/angular/src/styles.scss b/desktop/angular/src/styles.scss new file mode 100644 index 00000000..52a47745 --- /dev/null +++ b/desktop/angular/src/styles.scss @@ -0,0 +1,120 @@ +// +// Import our complete theme, order is important! +// +@import 'theme/_colors.scss'; +@import 'theme/_tailwind.scss'; +@import '@angular/cdk/overlay-prebuilt'; +@import 'theme/_button.scss'; +@import 'theme/_drag-n-drop.scss'; +@import 'theme/_inputs.scss'; +@import 'theme/_scroll.scss'; +@import 'theme/_search.scss'; +@import 'theme/_trust-level.scss'; +@import 'theme/_verdict.scss'; +@import 'theme/_typography.scss'; +@import 'theme/_markdown.scss'; +@import 'theme/_card.scss'; +@import 'theme/_breadcrumbs.scss'; +@import 'theme/_dialog.scss'; +@import 'theme/_table.scss'; +@import 'theme/_pill.scss'; + +@import 'safing/ui/theming'; + +*[routerlink] { + cursor: pointer; +} + +.form-field { + display: flex; + justify-content: flext-start; + align-items: center; + + *:not(:last-child) { + @apply mr-1; + } +} + +.sidebar { + @apply bg-background; + height: 100vh; + flex-shrink: 0; + flex-grow: 0; + @apply px-2; + display: flex; + flex-direction: column; + + &.no-scroll { + @apply px-0; + } +} + +.main { + .content { + flex-grow: 1; + @apply pl-12; + @apply pr-16; + @apply mr-4; + overflow: auto; + } + + .header { + display: flex; + width: 100%; + @apply pl-12; + @apply pr-5; + @apply mb-2; + align-items: center; + height: 3rem; + flex-shrink: 0; + + &:first-of-type { + @apply mt-2; + } + + >* { + flex-grow: 1; + margin: 0; + } + + >app-expertise { + flex-grow: 0; + flex-shrink: 0; + } + } +} + +.tableFixHead { + overflow-y: auto; +} + +.tableFixHead thead th { + position: sticky; + top: 0; +} + + +fa-icon.tipup, +fa-icon[icon="question-circle"], +fa-icon[icon="question"] { + max-width: 10px; + max-height: 10px; + opacity: 0.8; + display: inline-block; + font-size: 0.75rem; + color: rgb(250 250 250 / 55%); + margin-left: 3px; + + &:hover { + opacity: unset; + } +} + +.tipup-preview { + transition: all .25s ease-in-out !important; + opacity: 0 !important; + + &.visible { + opacity: 1 !important; + } +} diff --git a/desktop/angular/src/test.ts b/desktop/angular/src/test.ts new file mode 100644 index 00000000..06aa8e41 --- /dev/null +++ b/desktop/angular/src/test.ts @@ -0,0 +1,14 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); diff --git a/desktop/angular/src/theme.less b/desktop/angular/src/theme.less new file mode 100644 index 00000000..64154716 --- /dev/null +++ b/desktop/angular/src/theme.less @@ -0,0 +1,4 @@ + +// Custom Theming for NG-ZORRO +// For more information: https://ng.ant.design/docs/customize-theme/en +@import "../node_modules/ng-zorro-antd/ng-zorro-antd.dark.less"; diff --git a/desktop/angular/src/theme/_breadcrumbs.scss b/desktop/angular/src/theme/_breadcrumbs.scss new file mode 100644 index 00000000..42df118b --- /dev/null +++ b/desktop/angular/src/theme/_breadcrumbs.scss @@ -0,0 +1,20 @@ +h4.breadcrumbs { + * { + margin-left : 0.125rem; + margin-right: 0.125rem; + } + + span { + outline: none; + @apply text-secondary; + + &:hover { + @apply text-primary; + text-decoration: underline; + } + + &:last-of-type { + @apply text-primary; + } + } +} diff --git a/desktop/angular/src/theme/_button.scss b/desktop/angular/src/theme/_button.scss new file mode 100644 index 00000000..39012ee7 --- /dev/null +++ b/desktop/angular/src/theme/_button.scss @@ -0,0 +1,58 @@ +@layer components { + button { + @apply text-xs; + @apply bg-buttons-dark; + @apply p-1; + @apply px-4; + @apply capitalize; + @apply rounded-sm; + @apply font-medium; + user-select: none; + outline: none; + cursor: pointer; + font-size: 0.7rem; + + &.btn-outline { + background: transparent; + opacity: 0.6; + } + + &:hover { + &:not(.outline):not(.bg-blue) { + @apply bg-buttons-light; + } + + opacity: 1; + } + + &:disabled { + @apply cursor-default; + opacity: 0.3; + + &:not(.outline):hover { + @apply bg-buttons-dark; + } + } + + &:active { + @apply bg-buttons-dark; + } + + &:hover, + &:focus, + &:active { + outline: none; + } + } + + .info-circle { + width: 18px; + height: 18px; + display: flex; + justify-content: center; + align-items: center; + font-size: 0.8em; + @apply rounded-full; + @apply bg-buttons-dark; + } +} diff --git a/desktop/angular/src/theme/_card.scss b/desktop/angular/src/theme/_card.scss new file mode 100644 index 00000000..284a4bbe --- /dev/null +++ b/desktop/angular/src/theme/_card.scss @@ -0,0 +1,110 @@ +.card-header { + display : flex; + align-items : center; + cursor : pointer; + outline : none; + justify-content: space-between; + @apply text-xs; + @apply font-medium; + margin-top : 5px; + padding-top : 0.65rem; + padding-bottom : 0.65rem; + padding-left : 0.65rem; + padding-right : 0.65rem; + border-top-left-radius : 4px; + border-top-right-radius: 4px; + background-color : #202020e0; + + &:not(.open) { + border-radius: 4px; + } + + &>*:not(:last-child) { + @apply mr-1; + } + + &>app-icon:not(:last-child) { + @apply mr-2; + } + + &:hover { + background-color: #292929b0; + } + + &.active { + background-color: #303030; + + app-count-indicator { + background-color: #474747; + + div.state { + background-color: #5c5c5c; + + } + } + } + + &>app-icon { + --app-icon-size: 22px; + } + + .card-title { + flex-grow : 1; + overflow : hidden; + white-space : nowrap; + text-overflow: ellipsis; + font-size : 0.7rem; + font-weight : 600; + color : #cacaca; + margin-left : 3px; + + .card-sub-title { + display : block; + font-size : 0.8em; + margin-top: -3px; + @apply text-tertiary; + text-overflow: ellipsis; + overflow : hidden; + } + } + + .card-actions { + @apply mr-2; + + span { + display : inline-block; + text-align: center; + min-width : 5rem; + @apply px-2; + @apply rounded; + @apply text-xs; + + padding-top : 0.1rem; + padding-bottom: 0.1rem; + + // TODO(ppacher): this is actually a "toggle-switch" / radio-button + // component. make it one. + &.selected { + @apply bg-buttons-dark; + } + + &:hover { + @apply bg-buttons-light; + } + } + } +} + +.card-content { + @apply bg-cards-secondary; + @apply rounded-b; + + @apply py-2; + @apply px-4; + @apply mb-2; + + display : flex; + flex-direction : column; + flex : 1 0; + justify-content: space-between; +} diff --git a/desktop/angular/src/theme/_colors.scss b/desktop/angular/src/theme/_colors.scss new file mode 100644 index 00000000..65310698 --- /dev/null +++ b/desktop/angular/src/theme/_colors.scss @@ -0,0 +1,46 @@ +/** + * For debugging purposes, we define all our colors as + * CSS3 variables and make tailwind put a reference to those + * variables. This way we will see the variable name in the + * developer-tools instead of the hex/rgba values. + * + * You're welcome 🚀 + */ +:root { + --background: #121213; + + --text-primary : #ffffff; + --text-secondary: #ababab; + --text-tertiary : #888888; + + --cards-primary : #222222; + --cards-secondary : #1b1b1b; + --cards-secondary-rgb: 27, 27, 27; + --cards-tertiary : #2c2c2c; + + --button-icon : #ababab; + --button-dark : #343434; + --button-light: #474747; + + --info-green : #3df57f; + --info-red : #d12e2e; + --info-gray : #ababab; + --info-blue : #4e97fa; + --info-yellow : #e9d31d; + --info-yellow-rgb: 233, 211, 29; + + --protection-ok-primary : rgb(29, 233, 102); + --protection-ok-secondary: rgb(24, 130, 61); + --protection-ok-tertiary : rgb(20, 61, 36); + + --protection-warn-primary : rgb(233, 216, 29); + --protection-warn-secondary: rgb(130, 121, 24); + --protection-warn-tertiary : rgb(61, 58, 20); + + --protection-fail-primary : rgb(224, 29, 29); + --protection-fail-secondary: rgb(129, 24, 24); + --protection-fail-tertiary : rgb(61, 20, 20); + + --portmaster-plus: #2fcfae; + --portmaster-pro: #029ad0; +} diff --git a/desktop/angular/src/theme/_dialog.scss b/desktop/angular/src/theme/_dialog.scss new file mode 100644 index 00000000..c4eaaabe --- /dev/null +++ b/desktop/angular/src/theme/_dialog.scss @@ -0,0 +1,9 @@ +.dialog-screen-backdrop { + backdrop-filter : blur(10px); + background-color: rgba(#000000, 0.7); +} + +.dialog-screen-backdrop-light { + backdrop-filter : blur(3px); + background-color: rgba(#000000, 0.4); +} diff --git a/desktop/angular/src/theme/_drag-n-drop.scss b/desktop/angular/src/theme/_drag-n-drop.scss new file mode 100644 index 00000000..e6c69add --- /dev/null +++ b/desktop/angular/src/theme/_drag-n-drop.scss @@ -0,0 +1,46 @@ +.cdk-drag { + .widget { + user-select: none; + + fa-icon { + opacity: 1; + } + } +} + +.cdk-drag-placeholder { + user-select: none; + position: relative; + opacity: 0.5; + box-sizing: border-box; + cursor: grabbing !important; + @apply border-2; + @apply rounded; + @apply border-dashed; + border-color: #292929; +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); + user-select: none; +} + +.cdk-drag-preview { + user-select: none; + box-sizing: border-box; + cursor: grabbing !important; + + @apply rounded; + @apply border-2; + @apply border-dashed; + border-color: #292929; + @apply text-primary; +} + +.cdk-drag-handle { + cursor: grab !important; +} + +.document-grabbing { + cursor: grabbing !important; +} diff --git a/desktop/angular/src/theme/_inputs.scss b/desktop/angular/src/theme/_inputs.scss new file mode 100644 index 00000000..a7baf154 --- /dev/null +++ b/desktop/angular/src/theme/_inputs.scss @@ -0,0 +1,35 @@ +input:not([type="checkbox"]), +textarea, +select { + @apply outline-none w-full block; + @apply bg-gray-300 rounded; + @apply text-xs text-primary; + @apply border border-gray-300; + @apply rounded-sm font-medium; + @apply p-1.5; + + transition: border cubic-bezier(0.175, 0.885, 0.32, 1.275) .3s; + + &::placeholder { + @apply text-secondary text-xxs; + } + + &:active, + &:focus { + @apply text-primary; + @apply bg-gray-500 border-gray-400 bg-opacity-75 border-opacity-75; + + &::placeholder { + @apply text-tertiary; + } + } +} + + +input, +textarea, +select { + .ng-invalid { + @apply border-red; + } +} diff --git a/desktop/angular/src/theme/_markdown.scss b/desktop/angular/src/theme/_markdown.scss new file mode 100644 index 00000000..4bf2fa09 --- /dev/null +++ b/desktop/angular/src/theme/_markdown.scss @@ -0,0 +1,455 @@ +// Mostly taken from https://github.com/sindresorhus/github-markdown-css/blob/gh-pages/license + +markdown { + width: 100%; + @apply p-2; + color: white !important; + @apply font-normal; + + details { + display: block; + } + + summary { + display: list-item; + } + + a { + background-color: initial; + } + + a:active, + a:hover { + outline-width: 0; + } + + strong { + font-weight: inherit; + font-weight: bolder; + } + + h1 { + font-size: 2rem; + margin: .67rem 0; + } + + img { + border-style: none; + } + + code, + kbd, + pre { + font-family: monospace, monospace; + font-size: 1rem; + } + + hr { + box-sizing: initial; + height: 0; + overflow: visible; + } + + input { + font: inherit; + margin: 0; + } + + input { + overflow: visible; + } + + [type=checkbox] { + box-sizing: border-box; + padding: 0; + } + + * { + box-sizing: border-box; + } + + input { + font-family: inherit; + font-size: inherit; + line-height: inherit; + } + + a { + text-decoration: none; + } + + a:hover { + text-decoration: underline; + } + + strong { + font-weight: 600; + } + + hr { + height: 0; + margin: 15px 0; + overflow: hidden; + background: transparent; + border: 0; + border-bottom: 1px solid #dfe2e5; + } + + hr:after, + hr:before { + display: table; + content: ""; + } + + hr:after { + clear: both; + } + + table { + border-spacing: 0; + border-collapse: collapse; + } + + td, + th { + padding: 0; + } + + details summary { + cursor: pointer; + } + + kbd { + display: inline-block; + padding: 3px 5px; + font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; + line-height: 10px; + vertical-align: middle; + background-color: #fafbfc; + border: 1px solid #d1d5da; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #d1d5da; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + margin-top: 0; + margin-bottom: 0; + } + + h1 { + font-size: 32px; + } + + h1, + h2 { + font-weight: 600; + } + + h2 { + font-size: 24px; + } + + h3 { + font-size: 20px; + } + + h3, + h4 { + font-weight: 600; + } + + h4 { + font-size: 16px; + } + + h5 { + font-size: 14px; + } + + h5, + h6 { + font-weight: 600; + } + + h6 { + font-size: 12px; + } + + p { + margin-top: 0; + margin-bottom: 10px; + } + + blockquote { + margin: 0; + } + + ol, + ul { + padding-left: 0; + margin-top: 0; + margin-bottom: 0; + } + + ol { + list-style-type: decimal; + } + + ul { + list-style-type: circle; + } + + ol ol, + ul ol { + list-style-type: lower-roman; + } + + ol ol ol, + ol ul ol, + ul ol ol, + ul ul ol { + list-style-type: lower-alpha; + } + + dd { + margin-left: 0; + } + + code, + pre { + font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; + font-size: 12px; + } + + pre { + margin-top: 0; + margin-bottom: 0; + } + + input::-webkit-inner-spin-button, + input::-webkit-outer-spin-button { + margin: 0; + -webkit-appearance: none; + appearance: none; + } + + a:not([href]) { + color: inherit; + text-decoration: none; + } + + blockquote, + details, + dl, + ol, + p, + pre, + table, + ul { + margin-top: 0; + // be carefully when ever changing this! + margin-bottom: 16px; + } + + hr { + height: .25rem; + padding: 0; + margin: 24px 0; + background-color: #e1e4e8; + border: 0; + } + + blockquote { + padding: 0 1rem; + border-left: .25rem solid #dfe2e5; + } + + blockquote>:first-child { + margin-top: 0; + } + + blockquote>:last-child { + margin-bottom: 0; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; + } + + h1 { + font-size: 2rem; + } + + h1, + h2 { + padding-bottom: .3rem; + border-bottom: 1px solid #eaecef; + } + + h2 { + font-size: 1.5rem; + } + + h3 { + font-size: 1.25rem; + } + + h4 { + font-size: 1rem; + } + + h5 { + font-size: .875rem; + } + + h6 { + font-size: .85rem; + } + + ol, + ul { + padding-left: 2rem; + } + + ol ol, + ol ul, + ul ol, + ul ul { + margin-top: 0; + margin-bottom: 0; + } + + li { + word-wrap: break-all; + } + + li>p { + margin-top: 16px; + } + + li+li { + margin-top: .25rem; + } + + dl { + padding: 0; + } + + dl dt { + padding: 0; + margin-top: 16px; + font-size: 1rem; + font-style: italic; + font-weight: 600; + } + + dl dd { + padding: 0 16px; + margin-bottom: 16px; + } + + table { + display: block; + width: 100%; + overflow: auto; + } + + table th { + font-weight: 600; + } + + table td, + table th { + padding: 6px 13px; + border: 1px solid #dfe2e5; + } + + table tr { + background-color: #fff; + border-top: 1px solid #c6cbd1; + } + + table tr:nth-child(2n) { + background-color: #f6f8fa; + } + + img { + max-width: 100%; + box-sizing: initial; + background-color: #fff; + } + + img[align=right] { + padding-left: 20px; + } + + img[align=left] { + padding-right: 20px; + } + + code { + padding: .2rem .4rem; + margin: 0; + font-size: 95%; + background-color: rgba(27, 31, 35, .05); + border-radius: 3px; + } + + pre { + word-wrap: normal; + } + + pre>code { + padding: 0; + margin: 0; + font-size: 100%; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; + } + + .highlight { + margin-bottom: 16px; + } + + .highlight pre { + margin-bottom: 0; + word-break: normal; + } + + .highlight pre, + pre { + padding: 16px; + overflow: auto; + font-size: 90%; + line-height: 1.45; + background-color: #f6f8fa; + border-radius: 3px; + } + + pre code { + display: inline; + max-width: auto; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + background-color: initial; + border: 0; + } +} diff --git a/desktop/angular/src/theme/_pill.scss b/desktop/angular/src/theme/_pill.scss new file mode 100644 index 00000000..e85d9ac3 --- /dev/null +++ b/desktop/angular/src/theme/_pill.scss @@ -0,0 +1,7 @@ +@import 'mixins/_pill.scss'; + +.pill-container { + @include pill-container; + @apply pl-2; + @apply bg-buttons-dark; +} diff --git a/desktop/angular/src/theme/_scroll.scss b/desktop/angular/src/theme/_scroll.scss new file mode 100644 index 00000000..36ea80b0 --- /dev/null +++ b/desktop/angular/src/theme/_scroll.scss @@ -0,0 +1,28 @@ +html, +body { + scroll-behavior: smooth; +} + +::-webkit-scrollbar { + @apply bg-buttons-dark; + width: 4px; +} + +::-webkit-scrollbar-thumb { + @apply bg-buttons-light; + @apply rounded; + cursor: pointer; +} + +.no-scroll { + overflow: hidden; +} + +.scrollable { + width : 100%; + max-height: 100%; + overflow : auto; + overflow-x: hidden; + flex-grow : 1; + @apply px-3; +} diff --git a/desktop/angular/src/theme/_search.scss b/desktop/angular/src/theme/_search.scss new file mode 100644 index 00000000..d950cafd --- /dev/null +++ b/desktop/angular/src/theme/_search.scss @@ -0,0 +1,10 @@ +em.search-result { + @apply text-background; + @apply bg-yellow; + @apply border; + @apply border-yellow; + @apply rounded-sm; + + text-decoration: none; + font-style: inherit; +} diff --git a/desktop/angular/src/theme/_table.scss b/desktop/angular/src/theme/_table.scss new file mode 100644 index 00000000..035b5e17 --- /dev/null +++ b/desktop/angular/src/theme/_table.scss @@ -0,0 +1,41 @@ +table:not(.custom) { + width: 100%; + + th, + tr, + td { + @apply text-xs; + } + + th { + text-align: left; + @apply text-secondary; + z-index: 1; + } + + td, + th { + @apply p-2; + @apply font-medium; + } + + tr:nth-child(even) { + @apply bg-cards-secondary; + --bg-opacity: 0.5; + } + + tr:nth-child(odd) { + @apply bg-cards-tertiary; + --bg-opacity: 0.6; + } + + tr.cdk-header-row th { + @apply bg-cards-tertiary; + --bg-opacity: 1; + + // we cannot use borders directly due to + // the sticky header. Use a box-shadow to + // simulate a border. + box-shadow: 0 2px rgba(0, 0, 0, 0.3); + } +} diff --git a/desktop/angular/src/theme/_tailwind.scss b/desktop/angular/src/theme/_tailwind.scss new file mode 100644 index 00000000..28ccd29f --- /dev/null +++ b/desktop/angular/src/theme/_tailwind.scss @@ -0,0 +1,4 @@ +/** The tailwind post-processor will inject all tailwind styles here **/ +@import 'tailwindcss/base'; +@import 'tailwindcss/components'; +@import 'tailwindcss/utilities'; diff --git a/desktop/angular/src/theme/_trust-level.scss b/desktop/angular/src/theme/_trust-level.scss new file mode 100644 index 00000000..a4b00b51 --- /dev/null +++ b/desktop/angular/src/theme/_trust-level.scss @@ -0,0 +1,73 @@ +span.trust-level { + display : inline-block; + position : relative; + width : 6px; + user-select: none; + overflow : visible; + + &~* { + @apply ml-2; + } + + &:before { + content : ""; + display : block; + position : relative; + height : 6px; + width : 6px; + top : -1px; + left : 0px; + border-radius: 50%; + } + + &.centered:before { + top: 0px; + } + + &:before { + background-color: var(--bg-color); + @apply shadow-inner-xs; + } + + &.pulse:before { + animation : pulsate-trust 1s ease-out infinite; + box-shadow: 0 0 10px var(--glow-color); + } + + &.off { + --bg-color : theme('colors.info.gray'); + --glow-color: theme('colors.info.gray'); + } + + &.auto { + --bg-color : theme('colors.info.blue'); + --glow-color: theme('colors.info.blue'); + } + + &.low { + --bg-color : theme('colors.info.green'); + --glow-color: theme('colors.info.green'); + } + + &.medium { + --bg-color : theme('colors.info.yellow'); + --glow-color: theme('colors.info.yellow'); + } + + &.high { + --bg-color : theme('colors.info.red'); + --glow-color: theme('colors.info.red'); + } +} + +@keyframes pulsate-trust { + 100% { + opacity: 0.8; + } + + 0% { + background: var(--glow-color); + box-shadow: 0 0 0 var(--glow-color); + opacity : 1; + } +} diff --git a/desktop/angular/src/theme/_typography.scss b/desktop/angular/src/theme/_typography.scss new file mode 100644 index 00000000..1c8a6d01 --- /dev/null +++ b/desktop/angular/src/theme/_typography.scss @@ -0,0 +1,61 @@ +html, +body { + font-family: 'Roboto', sans-serif; + @apply text-primary; + @apply font-medium; +} + +body, +.primary-text, +.secondary-text { + @apply text-xs; + @apply font-medium; +} + +label, +.secondary-text { + @apply text-secondary; +} + +.primary-text { + @apply text-primary; +} + +.tertiary-text { + @apply text-tertiary; +} + +h1, +h2, +h3 { + @apply text-primary; +} + +h1 { + display: block; + + @apply mb-1; + @apply text-xl; + @apply font-normal; + @apply mb-2; +} + +h2 { + @apply p-2; + @apply ml-2; + @apply text-lg; + @apply font-medium; + letter-spacing: -0.01rem; +} + +h3 { + @apply mb-1; + @apply text-base; + @apply font-medium; +} + +h4 { + @apply text-xs; + @apply font-medium; + @apply text-tertiary; +} diff --git a/desktop/angular/src/theme/_verdict.scss b/desktop/angular/src/theme/_verdict.scss new file mode 100644 index 00000000..9f4a2730 --- /dev/null +++ b/desktop/angular/src/theme/_verdict.scss @@ -0,0 +1,47 @@ +span.verdict { + display : inline-block; + position : relative; + width : 12px; + height : 9px; + align-self : center; + justify-self: center; + user-select : none; + overflow : visible; + + &:before { + content : ""; + display : block; + position : absolute; + height : 8px; + width : 8px; + top : 0px; + left : 0px; + border-radius : 50%; + background-color: var(--bg-color); + border : 1px solid var(--bg-color); + @apply shadow-inner-xs; + } + + &.failed { + --bg-color: theme('colors.info.yellow'); + } + + &.accept, + &.reroutetons, + &.reroutetotunnel { + --bg-color: theme('colors.info.green'); + } + + &.block, + &.drop { + --bg-color: theme('colors.info.red'); + } + + &.outdated { + &:before { + background-color: transparent; + border-color : var(--bg-color); + opacity : .85; + } + } +} diff --git a/desktop/angular/src/theme/mixins/_pill.scss b/desktop/angular/src/theme/mixins/_pill.scss new file mode 100644 index 00000000..130f5519 --- /dev/null +++ b/desktop/angular/src/theme/mixins/_pill.scss @@ -0,0 +1,42 @@ +@mixin pill-container { + display : flex; + width : auto; + height : 18px; + align-items : center; + justify-content: flex-end; + font-size : 0.6rem; + line-height : 18px; + + border-radius: 0.5rem; + transform : scale(0.95); + + .counter { + flex-grow : 1; + display : inline-block; + text-align : right; + padding-right: 4px; + padding-left : 2px; + color : #999999ee; + font-size : 0.65rem; + font-weight : 800; + width : max-content; + } + + .pill { + display : inline-block; + width : 29px; + height : 5px; + background-color: #686868; + border-radius : 1rem; + overflow : hidden; + margin-left : 0.2rem; + margin-right : 0.6rem; + + .percentage { + display : block; + height : 100%; + width : 75%; + background-color: #21ad58; + } + } +} diff --git a/desktop/angular/tailwind.config.js b/desktop/angular/tailwind.config.js new file mode 100644 index 00000000..ba4e7f11 --- /dev/null +++ b/desktop/angular/tailwind.config.js @@ -0,0 +1,127 @@ +const plugin = require("tailwindcss/plugin"); + +module.exports = { + content: [ + "./src/**/*.{html,scss,css,ts}", + "./projects/**/*.{html,scss,css,ts}", + ], + theme: { + colors: { + transparent: "transparent", + current: "currentColor", + white: "#ffffff", + background: "#121213", + + gray: { + 100: "#131111", + 200: "#1b1b1b", + 300: "#222222", + 400: "#2c2c2c", + 500: "#474747", + 600: "#888888", + 700: "#ababab", + DEFAULT: "#ababab", + }, + + green: { + 100: "#143d24", + 200: "#18823d", + 300: "#1de966", + DEFAULT: "#18823d", + }, + + red: { + 100: "#3d1414", + 200: "#811818", + 300: "#e01d1d", + DEFAULT: "#d12e2e", + }, + + yellow: { + 100: "#3d3a14", + 200: "#827918", + 300: "#e9d81d", + DEFAULT: "#e9d81d", + }, + + cyan: { + 100: "#b2ebf2", + 200: "#80deea", + 300: "#4dd0e1", + 400: "#26c6da", + 500: "#00bcd4", + 600: "#00acc1", + 700: "#0097a7", + 800: "#00838f", + 900: "#006064", + }, + + deepPurple: { + 50: "#ede7f6", + 100: "#d1c4e9", + 200: "#b39ddb", + 300: "#9575cd", + 400: "#7e57c2", + 500: "#673ab7", + 600: "#5e35b1", + 700: "#512da8", + 800: "#4527a0", + 900: "#311b92", + }, + + blue: { + DEFAULT: "#4e97fa", + }, + + // Legacy color definitions + + // The overall application background color + + // Text shades + cards: { + primary: "var(--cards-primary)", + secondary: "var(--cards-secondary)", + tertiary: "var(--cards-tertiary)", + }, + + buttons: { + icon: "var(--button-icon)", + dark: "var(--button-dark)", + light: "var(--button-light)", + }, + + info: { + green: "var(--info-green)", + red: "var(--info-red)", + gray: "var(--info-gray)", + blue: "var(--info-blue)", + yellow: "var(--info-yellow)", + }, + }, + textColor: (theme) => { + return { + primary: theme("colors.white"), + secondary: theme("colors.gray.700"), + tertiary: theme("colors.gray.600"), + + ...theme("colors"), + }; + }, + extend: { + boxShadow: { + xs: "0 0 0 1px rgba(0, 0, 0, 0.05)", + "inner-xs": "inset 0 2px 4px 0 rgba(0, 0, 0, 0.16)", + }, + fontSize: { + xxs: "0.7rem", + }, + }, + }, + plugins: [ + plugin(function ({ addVariant, theme }) { + Object.keys(theme("screens")).forEach((key) => { + addVariant("sfng-" + key, ".min-width-" + theme("screens")[key] + " &"); + }); + }), + ], +}; diff --git a/desktop/angular/tsconfig.app.json b/desktop/angular/tsconfig.app.json new file mode 100644 index 00000000..f67c4660 --- /dev/null +++ b/desktop/angular/tsconfig.app.json @@ -0,0 +1,16 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [ + ] + }, + "files": [ + "src/main.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/desktop/angular/tsconfig.json b/desktop/angular/tsconfig.json new file mode 100644 index 00000000..281e9628 --- /dev/null +++ b/desktop/angular/tsconfig.json @@ -0,0 +1,41 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "paths": { + "@safing/portmaster-api": [ + "dist-lib/safing/portmaster-api" + ], + "@safing/ui": [ + "dist-lib/safing/ui" + ] + }, + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "es2020", + "lib": [ + "es2018", + "dom" + ], + "types": [ + "./src/electron-app.d.ts", + "chrome" + ], + "useDefineForClassFields": false + }, + "angularCompilerOptions": { + "strictInjectionParameters": true, + "strictTemplates": true + } +} diff --git a/desktop/angular/tsconfig.spec.json b/desktop/angular/tsconfig.spec.json new file mode 100644 index 00000000..800c6e2f --- /dev/null +++ b/desktop/angular/tsconfig.spec.json @@ -0,0 +1,19 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts", + "src/app/widgets/status-widget-factory/settings.ts" + ] +} diff --git a/desktop/angular/tslint.json b/desktop/angular/tslint.json new file mode 100644 index 00000000..eba6f798 --- /dev/null +++ b/desktop/angular/tslint.json @@ -0,0 +1,153 @@ +{ + "extends": "tslint:recommended", + "rules": { + "align": { + "options": [ + "parameters", + "statements" + ] + }, + "array-type": false, + "arrow-return-shorthand": true, + "curly": true, + "deprecation": { + "severity": "warning" + }, + "component-class-suffix": true, + "contextual-lifecycle": true, + "directive-class-suffix": true, + "directive-selector": [ + true, + "attribute", + "app", + "camelCase" + ], + "component-selector": [ + true, + "element", + "app", + "kebab-case" + ], + "eofline": true, + "import-blacklist": [ + true, + "rxjs/Rx" + ], + "import-spacing": true, + "indent": { + "options": [ + "spaces" + ] + }, + "max-classes-per-file": false, + "max-line-length": [ + true, + 140 + ], + "member-ordering": [ + true, + { + "order": [ + "static-field", + "instance-field", + "static-method", + "instance-method" + ] + } + ], + "no-any": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-empty": false, + "no-inferrable-types": [ + true, + "ignore-params" + ], + "no-non-null-assertion": true, + "no-redundant-jsdoc": true, + "no-switch-case-fall-through": true, + "no-var-requires": false, + "object-literal-key-quotes": [ + true, + "as-needed" + ], + "quotemark": [ + true, + "single" + ], + "semicolon": { + "options": [ + "always" + ] + }, + "space-before-function-paren": { + "options": { + "anonymous": "never", + "asyncArrow": "always", + "constructor": "never", + "method": "never", + "named": "never" + } + }, + "typedef": [ + true, + "call-signature" + ], + "typedef-whitespace": { + "options": [ + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + }, + { + "call-signature": "onespace", + "index-signature": "onespace", + "parameter": "onespace", + "property-declaration": "onespace", + "variable-declaration": "onespace" + } + ] + }, + "variable-name": { + "options": [ + "ban-keywords", + "check-format", + "allow-pascal-case" + ] + }, + "whitespace": { + "options": [ + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type", + "check-typecast" + ] + }, + "no-conflicting-lifecycle": true, + "no-host-metadata-property": true, + "no-input-rename": true, + "no-inputs-metadata-property": true, + "no-output-native": true, + "no-output-on-prefix": true, + "no-output-rename": true, + "no-outputs-metadata-property": true, + "template-banana-in-box": true, + "template-no-negated-async": true, + "use-lifecycle-interface": true, + "use-pipe-transform-interface": true + }, + "rulesDirectory": [ + "codelyzer" + ] +} \ No newline at end of file From ac23ce32a1376fd0358e1dc654b0940befbef7cd Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 20 Mar 2024 11:10:32 +0100 Subject: [PATCH 04/35] Migrate notifier from portmaster-ui to cmds/notifier, remove some duplicated code, move assets to assets/data and add a small go package in assets to allow embedding icons --- Earthfile | 9 +- .../{ => data}/fonts/Roboto-300/LICENSE.txt | 0 .../fonts/Roboto-300/Roboto-300.eot | Bin .../fonts/Roboto-300/Roboto-300.svg | 0 .../fonts/Roboto-300/Roboto-300.ttf | Bin .../fonts/Roboto-300/Roboto-300.woff | Bin .../fonts/Roboto-300/Roboto-300.woff2 | Bin .../fonts/Roboto-300italic/LICENSE.txt | 0 .../Roboto-300italic/Roboto-300italic.eot | Bin .../Roboto-300italic/Roboto-300italic.svg | 0 .../Roboto-300italic/Roboto-300italic.ttf | Bin .../Roboto-300italic/Roboto-300italic.woff | Bin .../Roboto-300italic/Roboto-300italic.woff2 | Bin .../{ => data}/fonts/Roboto-500/LICENSE.txt | 0 .../fonts/Roboto-500/Roboto-500.eot | Bin .../fonts/Roboto-500/Roboto-500.svg | 0 .../fonts/Roboto-500/Roboto-500.ttf | Bin .../fonts/Roboto-500/Roboto-500.woff | Bin .../fonts/Roboto-500/Roboto-500.woff2 | Bin .../fonts/Roboto-500italic/LICENSE.txt | 0 .../Roboto-500italic/Roboto-500italic.eot | Bin .../Roboto-500italic/Roboto-500italic.svg | 0 .../Roboto-500italic/Roboto-500italic.ttf | Bin .../Roboto-500italic/Roboto-500italic.woff | Bin .../Roboto-500italic/Roboto-500italic.woff2 | Bin .../{ => data}/fonts/Roboto-700/LICENSE.txt | 0 .../fonts/Roboto-700/Roboto-700.eot | Bin .../fonts/Roboto-700/Roboto-700.svg | 0 .../fonts/Roboto-700/Roboto-700.ttf | Bin .../fonts/Roboto-700/Roboto-700.woff | Bin .../fonts/Roboto-700/Roboto-700.woff2 | Bin .../fonts/Roboto-700italic/LICENSE.txt | 0 .../Roboto-700italic/Roboto-700italic.eot | Bin .../Roboto-700italic/Roboto-700italic.svg | 0 .../Roboto-700italic/Roboto-700italic.ttf | Bin .../Roboto-700italic/Roboto-700italic.woff | Bin .../Roboto-700italic/Roboto-700italic.woff2 | Bin .../fonts/Roboto-italic/LICENSE.txt | 0 .../fonts/Roboto-italic/Roboto-italic.eot | Bin .../fonts/Roboto-italic/Roboto-italic.svg | 0 .../fonts/Roboto-italic/Roboto-italic.ttf | Bin .../fonts/Roboto-italic/Roboto-italic.woff | Bin .../fonts/Roboto-italic/Roboto-italic.woff2 | Bin .../fonts/Roboto-regular/LICENSE.txt | 0 .../fonts/Roboto-regular/Roboto-regular.eot | Bin .../fonts/Roboto-regular/Roboto-regular.svg | 0 .../fonts/Roboto-regular/Roboto-regular.ttf | Bin .../fonts/Roboto-regular/Roboto-regular.woff | Bin .../fonts/Roboto-regular/Roboto-regular.woff2 | Bin assets/{ => data}/fonts/roboto-slimfix.css | 0 assets/{ => data}/fonts/roboto.css | 0 assets/{ => data}/icons/README.md | 0 assets/{ => data}/icons/pm_dark_128.png | Bin assets/{ => data}/icons/pm_dark_256.png | Bin assets/{ => data}/icons/pm_dark_512.ico | Bin assets/{ => data}/icons/pm_dark_512.png | Bin assets/{ => data}/icons/pm_dark_blue_128.png | Bin assets/{ => data}/icons/pm_dark_blue_256.png | Bin assets/{ => data}/icons/pm_dark_blue_512.ico | Bin assets/{ => data}/icons/pm_dark_blue_512.png | Bin assets/{ => data}/icons/pm_dark_green_128.png | Bin assets/{ => data}/icons/pm_dark_green_256.png | Bin assets/{ => data}/icons/pm_dark_green_512.ico | Bin assets/{ => data}/icons/pm_dark_green_512.png | Bin assets/{ => data}/icons/pm_dark_red_128.png | Bin assets/{ => data}/icons/pm_dark_red_256.png | Bin assets/{ => data}/icons/pm_dark_red_512.ico | Bin assets/{ => data}/icons/pm_dark_red_512.png | Bin .../{ => data}/icons/pm_dark_yellow_128.png | Bin .../{ => data}/icons/pm_dark_yellow_256.png | Bin .../{ => data}/icons/pm_dark_yellow_512.ico | Bin .../{ => data}/icons/pm_dark_yellow_512.png | Bin assets/{ => data}/icons/pm_light_128.png | Bin assets/{ => data}/icons/pm_light_256.png | Bin assets/{ => data}/icons/pm_light_512.ico | Bin assets/{ => data}/icons/pm_light_512.png | Bin assets/{ => data}/icons/pm_light_blue_128.png | Bin assets/{ => data}/icons/pm_light_blue_256.png | Bin assets/{ => data}/icons/pm_light_blue_512.ico | Bin assets/{ => data}/icons/pm_light_blue_512.png | Bin .../{ => data}/icons/pm_light_green_128.png | Bin .../{ => data}/icons/pm_light_green_256.png | Bin .../{ => data}/icons/pm_light_green_512.ico | Bin .../{ => data}/icons/pm_light_green_512.png | Bin assets/{ => data}/icons/pm_light_red_128.png | Bin assets/{ => data}/icons/pm_light_red_256.png | Bin assets/{ => data}/icons/pm_light_red_512.ico | Bin assets/{ => data}/icons/pm_light_red_512.png | Bin .../{ => data}/icons/pm_light_yellow_128.png | Bin .../{ => data}/icons/pm_light_yellow_256.png | Bin .../{ => data}/icons/pm_light_yellow_512.ico | Bin .../{ => data}/icons/pm_light_yellow_512.png | Bin assets/{ => data}/img/Mobile.svg | 0 assets/{ => data}/img/flags/AD.png | Bin assets/{ => data}/img/flags/AE.png | Bin assets/{ => data}/img/flags/AF.png | Bin assets/{ => data}/img/flags/AG.png | Bin assets/{ => data}/img/flags/AI.png | Bin assets/{ => data}/img/flags/AL.png | Bin assets/{ => data}/img/flags/AM.png | Bin assets/{ => data}/img/flags/AN.png | Bin assets/{ => data}/img/flags/AO.png | Bin assets/{ => data}/img/flags/AQ.png | Bin assets/{ => data}/img/flags/AR.png | Bin assets/{ => data}/img/flags/AS.png | Bin assets/{ => data}/img/flags/AT.png | Bin assets/{ => data}/img/flags/AU.png | Bin assets/{ => data}/img/flags/AW.png | Bin assets/{ => data}/img/flags/AX.png | Bin assets/{ => data}/img/flags/AZ.png | Bin assets/{ => data}/img/flags/BA.png | Bin assets/{ => data}/img/flags/BB.png | Bin assets/{ => data}/img/flags/BD.png | Bin assets/{ => data}/img/flags/BE.png | Bin assets/{ => data}/img/flags/BF.png | Bin assets/{ => data}/img/flags/BG.png | Bin assets/{ => data}/img/flags/BH.png | Bin assets/{ => data}/img/flags/BI.png | Bin assets/{ => data}/img/flags/BJ.png | Bin assets/{ => data}/img/flags/BL.png | Bin assets/{ => data}/img/flags/BM.png | Bin assets/{ => data}/img/flags/BN.png | Bin assets/{ => data}/img/flags/BO.png | Bin assets/{ => data}/img/flags/BR.png | Bin assets/{ => data}/img/flags/BS.png | Bin assets/{ => data}/img/flags/BT.png | Bin assets/{ => data}/img/flags/BW.png | Bin assets/{ => data}/img/flags/BY.png | Bin assets/{ => data}/img/flags/BZ.png | Bin assets/{ => data}/img/flags/CA.png | Bin assets/{ => data}/img/flags/CC.png | Bin assets/{ => data}/img/flags/CD.png | Bin assets/{ => data}/img/flags/CF.png | Bin assets/{ => data}/img/flags/CG.png | Bin assets/{ => data}/img/flags/CH.png | Bin assets/{ => data}/img/flags/CI.png | Bin assets/{ => data}/img/flags/CK.png | Bin assets/{ => data}/img/flags/CL.png | Bin assets/{ => data}/img/flags/CM.png | Bin assets/{ => data}/img/flags/CN.png | Bin assets/{ => data}/img/flags/CO.png | Bin assets/{ => data}/img/flags/CR.png | Bin assets/{ => data}/img/flags/CT.png | Bin assets/{ => data}/img/flags/CU.png | Bin assets/{ => data}/img/flags/CV.png | Bin assets/{ => data}/img/flags/CW.png | Bin assets/{ => data}/img/flags/CX.png | Bin assets/{ => data}/img/flags/CY.png | Bin assets/{ => data}/img/flags/CZ.png | Bin assets/{ => data}/img/flags/DE.png | Bin assets/{ => data}/img/flags/DJ.png | Bin assets/{ => data}/img/flags/DK.png | Bin assets/{ => data}/img/flags/DM.png | Bin assets/{ => data}/img/flags/DO.png | Bin assets/{ => data}/img/flags/DZ.png | Bin assets/{ => data}/img/flags/EC.png | Bin assets/{ => data}/img/flags/EE.png | Bin assets/{ => data}/img/flags/EG.png | Bin assets/{ => data}/img/flags/EH.png | Bin assets/{ => data}/img/flags/ER.png | Bin assets/{ => data}/img/flags/ES.png | Bin assets/{ => data}/img/flags/ET.png | Bin assets/{ => data}/img/flags/EU.png | Bin assets/{ => data}/img/flags/FI.png | Bin assets/{ => data}/img/flags/FJ.png | Bin assets/{ => data}/img/flags/FK.png | Bin assets/{ => data}/img/flags/FM.png | Bin assets/{ => data}/img/flags/FO.png | Bin assets/{ => data}/img/flags/FR.png | Bin assets/{ => data}/img/flags/GA.png | Bin assets/{ => data}/img/flags/GB.png | Bin assets/{ => data}/img/flags/GD.png | Bin assets/{ => data}/img/flags/GE.png | Bin assets/{ => data}/img/flags/GG.png | Bin assets/{ => data}/img/flags/GH.png | Bin assets/{ => data}/img/flags/GI.png | Bin assets/{ => data}/img/flags/GL.png | Bin assets/{ => data}/img/flags/GM.png | Bin assets/{ => data}/img/flags/GN.png | Bin assets/{ => data}/img/flags/GQ.png | Bin assets/{ => data}/img/flags/GR.png | Bin assets/{ => data}/img/flags/GS.png | Bin assets/{ => data}/img/flags/GT.png | Bin assets/{ => data}/img/flags/GU.png | Bin assets/{ => data}/img/flags/GW.png | Bin assets/{ => data}/img/flags/GY.png | Bin assets/{ => data}/img/flags/HK.png | Bin assets/{ => data}/img/flags/HN.png | Bin assets/{ => data}/img/flags/HR.png | Bin assets/{ => data}/img/flags/HT.png | Bin assets/{ => data}/img/flags/HU.png | Bin assets/{ => data}/img/flags/IC.png | Bin assets/{ => data}/img/flags/ID.png | Bin assets/{ => data}/img/flags/IE.png | Bin assets/{ => data}/img/flags/IL.png | Bin assets/{ => data}/img/flags/IM.png | Bin assets/{ => data}/img/flags/IN.png | Bin assets/{ => data}/img/flags/IQ.png | Bin assets/{ => data}/img/flags/IR.png | Bin assets/{ => data}/img/flags/IS.png | Bin assets/{ => data}/img/flags/IT.png | Bin assets/{ => data}/img/flags/JE.png | Bin assets/{ => data}/img/flags/JM.png | Bin assets/{ => data}/img/flags/JO.png | Bin assets/{ => data}/img/flags/JP.png | Bin assets/{ => data}/img/flags/KE.png | Bin assets/{ => data}/img/flags/KG.png | Bin assets/{ => data}/img/flags/KH.png | Bin assets/{ => data}/img/flags/KI.png | Bin assets/{ => data}/img/flags/KM.png | Bin assets/{ => data}/img/flags/KN.png | Bin assets/{ => data}/img/flags/KP.png | Bin assets/{ => data}/img/flags/KR.png | Bin assets/{ => data}/img/flags/KW.png | Bin assets/{ => data}/img/flags/KY.png | Bin assets/{ => data}/img/flags/KZ.png | Bin assets/{ => data}/img/flags/LA.png | Bin assets/{ => data}/img/flags/LB.png | Bin assets/{ => data}/img/flags/LC.png | Bin assets/{ => data}/img/flags/LI.png | Bin assets/{ => data}/img/flags/LICENSE.txt | 0 assets/{ => data}/img/flags/LK.png | Bin assets/{ => data}/img/flags/LR.png | Bin assets/{ => data}/img/flags/LS.png | Bin assets/{ => data}/img/flags/LT.png | Bin assets/{ => data}/img/flags/LU.png | Bin assets/{ => data}/img/flags/LV.png | Bin assets/{ => data}/img/flags/LY.png | Bin assets/{ => data}/img/flags/MA.png | Bin assets/{ => data}/img/flags/MC.png | Bin assets/{ => data}/img/flags/MD.png | Bin assets/{ => data}/img/flags/ME.png | Bin assets/{ => data}/img/flags/MF.png | Bin assets/{ => data}/img/flags/MG.png | Bin assets/{ => data}/img/flags/MH.png | Bin assets/{ => data}/img/flags/MK.png | Bin assets/{ => data}/img/flags/ML.png | Bin assets/{ => data}/img/flags/MM.png | Bin assets/{ => data}/img/flags/MN.png | Bin assets/{ => data}/img/flags/MO.png | Bin assets/{ => data}/img/flags/MP.png | Bin assets/{ => data}/img/flags/MQ.png | Bin assets/{ => data}/img/flags/MR.png | Bin assets/{ => data}/img/flags/MS.png | Bin assets/{ => data}/img/flags/MT.png | Bin assets/{ => data}/img/flags/MU.png | Bin assets/{ => data}/img/flags/MV.png | Bin assets/{ => data}/img/flags/MW.png | Bin assets/{ => data}/img/flags/MX.png | Bin assets/{ => data}/img/flags/MY.png | Bin assets/{ => data}/img/flags/MZ.png | Bin assets/{ => data}/img/flags/NA.png | Bin assets/{ => data}/img/flags/NC.png | Bin assets/{ => data}/img/flags/NE.png | Bin assets/{ => data}/img/flags/NF.png | Bin assets/{ => data}/img/flags/NG.png | Bin assets/{ => data}/img/flags/NI.png | Bin assets/{ => data}/img/flags/NL.png | Bin assets/{ => data}/img/flags/NO.png | Bin assets/{ => data}/img/flags/NP.png | Bin assets/{ => data}/img/flags/NR.png | Bin assets/{ => data}/img/flags/NU.png | Bin assets/{ => data}/img/flags/NZ.png | Bin assets/{ => data}/img/flags/OM.png | Bin assets/{ => data}/img/flags/PA.png | Bin assets/{ => data}/img/flags/PE.png | Bin assets/{ => data}/img/flags/PF.png | Bin assets/{ => data}/img/flags/PG.png | Bin assets/{ => data}/img/flags/PH.png | Bin assets/{ => data}/img/flags/PK.png | Bin assets/{ => data}/img/flags/PL.png | Bin assets/{ => data}/img/flags/PN.png | Bin assets/{ => data}/img/flags/PR.png | Bin assets/{ => data}/img/flags/PS.png | Bin assets/{ => data}/img/flags/PT.png | Bin assets/{ => data}/img/flags/PW.png | Bin assets/{ => data}/img/flags/PY.png | Bin assets/{ => data}/img/flags/QA.png | Bin assets/{ => data}/img/flags/RE.png | Bin assets/{ => data}/img/flags/RO.png | Bin assets/{ => data}/img/flags/RS.png | Bin assets/{ => data}/img/flags/RU.png | Bin assets/{ => data}/img/flags/RW.png | Bin assets/{ => data}/img/flags/SA.png | Bin assets/{ => data}/img/flags/SB.png | Bin assets/{ => data}/img/flags/SC.png | Bin assets/{ => data}/img/flags/SD.png | Bin assets/{ => data}/img/flags/SE.png | Bin assets/{ => data}/img/flags/SG.png | Bin assets/{ => data}/img/flags/SH.png | Bin assets/{ => data}/img/flags/SI.png | Bin assets/{ => data}/img/flags/SK.png | Bin assets/{ => data}/img/flags/SL.png | Bin assets/{ => data}/img/flags/SM.png | Bin assets/{ => data}/img/flags/SN.png | Bin assets/{ => data}/img/flags/SO.png | Bin assets/{ => data}/img/flags/SR.png | Bin assets/{ => data}/img/flags/SS.png | Bin assets/{ => data}/img/flags/ST.png | Bin assets/{ => data}/img/flags/SV.png | Bin assets/{ => data}/img/flags/SX.png | Bin assets/{ => data}/img/flags/SY.png | Bin assets/{ => data}/img/flags/SZ.png | Bin assets/{ => data}/img/flags/TC.png | Bin assets/{ => data}/img/flags/TD.png | Bin assets/{ => data}/img/flags/TF.png | Bin assets/{ => data}/img/flags/TG.png | Bin assets/{ => data}/img/flags/TH.png | Bin assets/{ => data}/img/flags/TJ.png | Bin assets/{ => data}/img/flags/TK.png | Bin assets/{ => data}/img/flags/TL.png | Bin assets/{ => data}/img/flags/TM.png | Bin assets/{ => data}/img/flags/TN.png | Bin assets/{ => data}/img/flags/TO.png | Bin assets/{ => data}/img/flags/TR.png | Bin assets/{ => data}/img/flags/TT.png | Bin assets/{ => data}/img/flags/TV.png | Bin assets/{ => data}/img/flags/TW.png | Bin assets/{ => data}/img/flags/TZ.png | Bin assets/{ => data}/img/flags/UA.png | Bin assets/{ => data}/img/flags/UG.png | Bin assets/{ => data}/img/flags/US.png | Bin assets/{ => data}/img/flags/UY.png | Bin assets/{ => data}/img/flags/UZ.png | Bin assets/{ => data}/img/flags/VA.png | Bin assets/{ => data}/img/flags/VC.png | Bin assets/{ => data}/img/flags/VE.png | Bin assets/{ => data}/img/flags/VG.png | Bin assets/{ => data}/img/flags/VI.png | Bin assets/{ => data}/img/flags/VN.png | Bin assets/{ => data}/img/flags/VU.png | Bin assets/{ => data}/img/flags/WF.png | Bin assets/{ => data}/img/flags/WS.png | Bin assets/{ => data}/img/flags/YE.png | Bin assets/{ => data}/img/flags/YT.png | Bin assets/{ => data}/img/flags/ZA.png | Bin assets/{ => data}/img/flags/ZM.png | Bin assets/{ => data}/img/flags/ZW.png | Bin assets/{ => data}/img/flags/_abkhazia.png | Bin .../{ => data}/img/flags/_basque-country.png | Bin .../flags/_british-antarctic-territory.png | Bin assets/{ => data}/img/flags/_commonwealth.png | Bin assets/{ => data}/img/flags/_england.png | Bin assets/{ => data}/img/flags/_gosquared.png | Bin assets/{ => data}/img/flags/_kosovo.png | Bin assets/{ => data}/img/flags/_mars.png | Bin .../img/flags/_nagorno-karabakh.png | Bin assets/{ => data}/img/flags/_nato.png | Bin .../{ => data}/img/flags/_northern-cyprus.png | Bin assets/{ => data}/img/flags/_olympics.png | Bin assets/{ => data}/img/flags/_red-cross.png | Bin assets/{ => data}/img/flags/_scotland.png | Bin assets/{ => data}/img/flags/_somaliland.png | Bin .../{ => data}/img/flags/_south-ossetia.png | Bin .../{ => data}/img/flags/_united-nations.png | Bin assets/{ => data}/img/flags/_unknown.png | Bin assets/{ => data}/img/flags/_wales.png | Bin assets/{ => data}/img/linux.svg | 0 assets/{ => data}/img/mac.svg | 0 assets/{ => data}/img/plants1-br.png | Bin assets/{ => data}/img/plants1.png | Bin .../access-regional-content-easily.png | Bin .../built-from-the-ground-up.png | Bin .../img/spn-feature-carousel/bye-bye-vpns.png | Bin .../easily-control-your-privacy.png | Bin .../multiple-identities-for-each-app.png | Bin assets/{ => data}/img/spn-login.png | Bin assets/{ => data}/img/windows.svg | 0 assets/{ => data}/world-50m.json | 0 assets/icons.go | 8 + assets/icons_default.go | 102 +++++++ assets/icons_windows.go | 41 +++ cmds/notifier/.gitignore | 34 +++ cmds/notifier/README.md | 5 + cmds/notifier/http_api.go | 65 ++++ cmds/notifier/icons.go | 25 ++ cmds/notifier/main.go | 286 ++++++++++++++++++ cmds/notifier/notification.go | 36 +++ cmds/notifier/notify.go | 103 +++++++ cmds/notifier/notify_linux.go | 154 ++++++++++ cmds/notifier/notify_windows.go | 184 +++++++++++ cmds/notifier/shutdown.go | 50 +++ cmds/notifier/snoretoast-guid.patch | 15 + cmds/notifier/spn.go | 103 +++++++ cmds/notifier/subsystems.go | 122 ++++++++ cmds/notifier/tray.go | 218 +++++++++++++ .../notifier/wintoast/notification_builder.go | 90 ++++++ cmds/notifier/wintoast/wintoast.go | 217 +++++++++++++ desktop/angular/assets | 2 +- go.mod | 5 +- go.sum | 8 + runtime/.gitkeep | 1 - 392 files changed, 1879 insertions(+), 4 deletions(-) rename assets/{ => data}/fonts/Roboto-300/LICENSE.txt (100%) rename assets/{ => data}/fonts/Roboto-300/Roboto-300.eot (100%) rename assets/{ => data}/fonts/Roboto-300/Roboto-300.svg (100%) rename assets/{ => data}/fonts/Roboto-300/Roboto-300.ttf (100%) rename assets/{ => data}/fonts/Roboto-300/Roboto-300.woff (100%) rename assets/{ => data}/fonts/Roboto-300/Roboto-300.woff2 (100%) rename assets/{ => data}/fonts/Roboto-300italic/LICENSE.txt (100%) rename assets/{ => data}/fonts/Roboto-300italic/Roboto-300italic.eot (100%) rename assets/{ => data}/fonts/Roboto-300italic/Roboto-300italic.svg (100%) rename assets/{ => data}/fonts/Roboto-300italic/Roboto-300italic.ttf (100%) rename assets/{ => data}/fonts/Roboto-300italic/Roboto-300italic.woff (100%) rename assets/{ => data}/fonts/Roboto-300italic/Roboto-300italic.woff2 (100%) rename assets/{ => data}/fonts/Roboto-500/LICENSE.txt (100%) rename assets/{ => data}/fonts/Roboto-500/Roboto-500.eot (100%) rename assets/{ => data}/fonts/Roboto-500/Roboto-500.svg (100%) rename assets/{ => data}/fonts/Roboto-500/Roboto-500.ttf (100%) rename assets/{ => data}/fonts/Roboto-500/Roboto-500.woff (100%) rename assets/{ => data}/fonts/Roboto-500/Roboto-500.woff2 (100%) rename assets/{ => data}/fonts/Roboto-500italic/LICENSE.txt (100%) rename assets/{ => data}/fonts/Roboto-500italic/Roboto-500italic.eot (100%) rename assets/{ => data}/fonts/Roboto-500italic/Roboto-500italic.svg (100%) rename assets/{ => data}/fonts/Roboto-500italic/Roboto-500italic.ttf (100%) rename assets/{ => data}/fonts/Roboto-500italic/Roboto-500italic.woff (100%) rename assets/{ => data}/fonts/Roboto-500italic/Roboto-500italic.woff2 (100%) rename assets/{ => data}/fonts/Roboto-700/LICENSE.txt (100%) rename assets/{ => data}/fonts/Roboto-700/Roboto-700.eot (100%) rename assets/{ => data}/fonts/Roboto-700/Roboto-700.svg (100%) rename assets/{ => data}/fonts/Roboto-700/Roboto-700.ttf (100%) rename assets/{ => data}/fonts/Roboto-700/Roboto-700.woff (100%) rename assets/{ => data}/fonts/Roboto-700/Roboto-700.woff2 (100%) rename assets/{ => data}/fonts/Roboto-700italic/LICENSE.txt (100%) rename assets/{ => data}/fonts/Roboto-700italic/Roboto-700italic.eot (100%) rename assets/{ => data}/fonts/Roboto-700italic/Roboto-700italic.svg (100%) rename assets/{ => data}/fonts/Roboto-700italic/Roboto-700italic.ttf (100%) rename assets/{ => data}/fonts/Roboto-700italic/Roboto-700italic.woff (100%) rename assets/{ => data}/fonts/Roboto-700italic/Roboto-700italic.woff2 (100%) rename assets/{ => data}/fonts/Roboto-italic/LICENSE.txt (100%) rename assets/{ => data}/fonts/Roboto-italic/Roboto-italic.eot (100%) rename assets/{ => data}/fonts/Roboto-italic/Roboto-italic.svg (100%) rename assets/{ => data}/fonts/Roboto-italic/Roboto-italic.ttf (100%) rename assets/{ => data}/fonts/Roboto-italic/Roboto-italic.woff (100%) rename assets/{ => data}/fonts/Roboto-italic/Roboto-italic.woff2 (100%) rename assets/{ => data}/fonts/Roboto-regular/LICENSE.txt (100%) rename assets/{ => data}/fonts/Roboto-regular/Roboto-regular.eot (100%) rename assets/{ => data}/fonts/Roboto-regular/Roboto-regular.svg (100%) rename assets/{ => data}/fonts/Roboto-regular/Roboto-regular.ttf (100%) rename assets/{ => data}/fonts/Roboto-regular/Roboto-regular.woff (100%) rename assets/{ => data}/fonts/Roboto-regular/Roboto-regular.woff2 (100%) rename assets/{ => data}/fonts/roboto-slimfix.css (100%) rename assets/{ => data}/fonts/roboto.css (100%) rename assets/{ => data}/icons/README.md (100%) rename assets/{ => data}/icons/pm_dark_128.png (100%) rename assets/{ => data}/icons/pm_dark_256.png (100%) rename assets/{ => data}/icons/pm_dark_512.ico (100%) rename assets/{ => data}/icons/pm_dark_512.png (100%) rename assets/{ => data}/icons/pm_dark_blue_128.png (100%) rename assets/{ => data}/icons/pm_dark_blue_256.png (100%) rename assets/{ => data}/icons/pm_dark_blue_512.ico (100%) rename assets/{ => data}/icons/pm_dark_blue_512.png (100%) rename assets/{ => data}/icons/pm_dark_green_128.png (100%) rename assets/{ => data}/icons/pm_dark_green_256.png (100%) rename assets/{ => data}/icons/pm_dark_green_512.ico (100%) rename assets/{ => data}/icons/pm_dark_green_512.png (100%) rename assets/{ => data}/icons/pm_dark_red_128.png (100%) rename assets/{ => data}/icons/pm_dark_red_256.png (100%) rename assets/{ => data}/icons/pm_dark_red_512.ico (100%) rename assets/{ => data}/icons/pm_dark_red_512.png (100%) rename assets/{ => data}/icons/pm_dark_yellow_128.png (100%) rename assets/{ => data}/icons/pm_dark_yellow_256.png (100%) rename assets/{ => data}/icons/pm_dark_yellow_512.ico (100%) rename assets/{ => data}/icons/pm_dark_yellow_512.png (100%) rename assets/{ => data}/icons/pm_light_128.png (100%) rename assets/{ => data}/icons/pm_light_256.png (100%) rename assets/{ => data}/icons/pm_light_512.ico (100%) rename assets/{ => data}/icons/pm_light_512.png (100%) rename assets/{ => data}/icons/pm_light_blue_128.png (100%) rename assets/{ => data}/icons/pm_light_blue_256.png (100%) rename assets/{ => data}/icons/pm_light_blue_512.ico (100%) rename assets/{ => data}/icons/pm_light_blue_512.png (100%) rename assets/{ => data}/icons/pm_light_green_128.png (100%) rename assets/{ => data}/icons/pm_light_green_256.png (100%) rename assets/{ => data}/icons/pm_light_green_512.ico (100%) rename assets/{ => data}/icons/pm_light_green_512.png (100%) rename assets/{ => data}/icons/pm_light_red_128.png (100%) rename assets/{ => data}/icons/pm_light_red_256.png (100%) rename assets/{ => data}/icons/pm_light_red_512.ico (100%) rename assets/{ => data}/icons/pm_light_red_512.png (100%) rename assets/{ => data}/icons/pm_light_yellow_128.png (100%) rename assets/{ => data}/icons/pm_light_yellow_256.png (100%) rename assets/{ => data}/icons/pm_light_yellow_512.ico (100%) rename assets/{ => data}/icons/pm_light_yellow_512.png (100%) rename assets/{ => data}/img/Mobile.svg (100%) rename assets/{ => data}/img/flags/AD.png (100%) rename assets/{ => data}/img/flags/AE.png (100%) rename assets/{ => data}/img/flags/AF.png (100%) rename assets/{ => data}/img/flags/AG.png (100%) rename assets/{ => data}/img/flags/AI.png (100%) rename assets/{ => data}/img/flags/AL.png (100%) rename assets/{ => data}/img/flags/AM.png (100%) rename assets/{ => data}/img/flags/AN.png (100%) rename assets/{ => data}/img/flags/AO.png (100%) rename assets/{ => data}/img/flags/AQ.png (100%) rename assets/{ => data}/img/flags/AR.png (100%) rename assets/{ => data}/img/flags/AS.png (100%) rename assets/{ => data}/img/flags/AT.png (100%) rename assets/{ => data}/img/flags/AU.png (100%) rename assets/{ => data}/img/flags/AW.png (100%) rename assets/{ => data}/img/flags/AX.png (100%) rename assets/{ => data}/img/flags/AZ.png (100%) rename assets/{ => data}/img/flags/BA.png (100%) rename assets/{ => data}/img/flags/BB.png (100%) rename assets/{ => data}/img/flags/BD.png (100%) rename assets/{ => data}/img/flags/BE.png (100%) rename assets/{ => data}/img/flags/BF.png (100%) rename assets/{ => data}/img/flags/BG.png (100%) rename assets/{ => data}/img/flags/BH.png (100%) rename assets/{ => data}/img/flags/BI.png (100%) rename assets/{ => data}/img/flags/BJ.png (100%) rename assets/{ => data}/img/flags/BL.png (100%) rename assets/{ => data}/img/flags/BM.png (100%) rename assets/{ => data}/img/flags/BN.png (100%) rename assets/{ => data}/img/flags/BO.png (100%) rename assets/{ => data}/img/flags/BR.png (100%) rename assets/{ => data}/img/flags/BS.png (100%) rename assets/{ => data}/img/flags/BT.png (100%) rename assets/{ => data}/img/flags/BW.png (100%) rename assets/{ => data}/img/flags/BY.png (100%) rename assets/{ => data}/img/flags/BZ.png (100%) rename assets/{ => data}/img/flags/CA.png (100%) rename assets/{ => data}/img/flags/CC.png (100%) rename assets/{ => data}/img/flags/CD.png (100%) rename assets/{ => data}/img/flags/CF.png (100%) rename assets/{ => data}/img/flags/CG.png (100%) rename assets/{ => data}/img/flags/CH.png (100%) rename assets/{ => data}/img/flags/CI.png (100%) rename assets/{ => data}/img/flags/CK.png (100%) rename assets/{ => data}/img/flags/CL.png (100%) rename assets/{ => data}/img/flags/CM.png (100%) rename assets/{ => data}/img/flags/CN.png (100%) rename assets/{ => data}/img/flags/CO.png (100%) rename assets/{ => data}/img/flags/CR.png (100%) rename assets/{ => data}/img/flags/CT.png (100%) rename assets/{ => data}/img/flags/CU.png (100%) rename assets/{ => data}/img/flags/CV.png (100%) rename assets/{ => data}/img/flags/CW.png (100%) rename assets/{ => data}/img/flags/CX.png (100%) rename assets/{ => data}/img/flags/CY.png (100%) rename assets/{ => data}/img/flags/CZ.png (100%) rename assets/{ => data}/img/flags/DE.png (100%) rename assets/{ => data}/img/flags/DJ.png (100%) rename assets/{ => data}/img/flags/DK.png (100%) rename assets/{ => data}/img/flags/DM.png (100%) rename assets/{ => data}/img/flags/DO.png (100%) rename assets/{ => data}/img/flags/DZ.png (100%) rename assets/{ => data}/img/flags/EC.png (100%) rename assets/{ => data}/img/flags/EE.png (100%) rename assets/{ => data}/img/flags/EG.png (100%) rename assets/{ => data}/img/flags/EH.png (100%) rename assets/{ => data}/img/flags/ER.png (100%) rename assets/{ => data}/img/flags/ES.png (100%) rename assets/{ => data}/img/flags/ET.png (100%) rename assets/{ => data}/img/flags/EU.png (100%) rename assets/{ => data}/img/flags/FI.png (100%) rename assets/{ => data}/img/flags/FJ.png (100%) rename assets/{ => data}/img/flags/FK.png (100%) rename assets/{ => data}/img/flags/FM.png (100%) rename assets/{ => data}/img/flags/FO.png (100%) rename assets/{ => data}/img/flags/FR.png (100%) rename assets/{ => data}/img/flags/GA.png (100%) rename assets/{ => data}/img/flags/GB.png (100%) rename assets/{ => data}/img/flags/GD.png (100%) rename assets/{ => data}/img/flags/GE.png (100%) rename assets/{ => data}/img/flags/GG.png (100%) rename assets/{ => data}/img/flags/GH.png (100%) rename assets/{ => data}/img/flags/GI.png (100%) rename assets/{ => data}/img/flags/GL.png (100%) rename assets/{ => data}/img/flags/GM.png (100%) rename assets/{ => data}/img/flags/GN.png (100%) rename assets/{ => data}/img/flags/GQ.png (100%) rename assets/{ => data}/img/flags/GR.png (100%) rename assets/{ => data}/img/flags/GS.png (100%) rename assets/{ => data}/img/flags/GT.png (100%) rename assets/{ => data}/img/flags/GU.png (100%) rename assets/{ => data}/img/flags/GW.png (100%) rename assets/{ => data}/img/flags/GY.png (100%) rename assets/{ => data}/img/flags/HK.png (100%) rename assets/{ => data}/img/flags/HN.png (100%) rename assets/{ => data}/img/flags/HR.png (100%) rename assets/{ => data}/img/flags/HT.png (100%) rename assets/{ => data}/img/flags/HU.png (100%) rename assets/{ => data}/img/flags/IC.png (100%) rename assets/{ => data}/img/flags/ID.png (100%) rename assets/{ => data}/img/flags/IE.png (100%) rename assets/{ => data}/img/flags/IL.png (100%) rename assets/{ => data}/img/flags/IM.png (100%) rename assets/{ => data}/img/flags/IN.png (100%) rename assets/{ => data}/img/flags/IQ.png (100%) rename assets/{ => data}/img/flags/IR.png (100%) rename assets/{ => data}/img/flags/IS.png (100%) rename assets/{ => data}/img/flags/IT.png (100%) rename assets/{ => data}/img/flags/JE.png (100%) rename assets/{ => data}/img/flags/JM.png (100%) rename assets/{ => data}/img/flags/JO.png (100%) rename assets/{ => data}/img/flags/JP.png (100%) rename assets/{ => data}/img/flags/KE.png (100%) rename assets/{ => data}/img/flags/KG.png (100%) rename assets/{ => data}/img/flags/KH.png (100%) rename assets/{ => data}/img/flags/KI.png (100%) rename assets/{ => data}/img/flags/KM.png (100%) rename assets/{ => data}/img/flags/KN.png (100%) rename assets/{ => data}/img/flags/KP.png (100%) rename assets/{ => data}/img/flags/KR.png (100%) rename assets/{ => data}/img/flags/KW.png (100%) rename assets/{ => data}/img/flags/KY.png (100%) rename assets/{ => data}/img/flags/KZ.png (100%) rename assets/{ => data}/img/flags/LA.png (100%) rename assets/{ => data}/img/flags/LB.png (100%) rename assets/{ => data}/img/flags/LC.png (100%) rename assets/{ => data}/img/flags/LI.png (100%) rename assets/{ => data}/img/flags/LICENSE.txt (100%) rename assets/{ => data}/img/flags/LK.png (100%) rename assets/{ => data}/img/flags/LR.png (100%) rename assets/{ => data}/img/flags/LS.png (100%) rename assets/{ => data}/img/flags/LT.png (100%) rename assets/{ => data}/img/flags/LU.png (100%) rename assets/{ => data}/img/flags/LV.png (100%) rename assets/{ => data}/img/flags/LY.png (100%) rename assets/{ => data}/img/flags/MA.png (100%) rename assets/{ => data}/img/flags/MC.png (100%) rename assets/{ => data}/img/flags/MD.png (100%) rename assets/{ => data}/img/flags/ME.png (100%) rename assets/{ => data}/img/flags/MF.png (100%) rename assets/{ => data}/img/flags/MG.png (100%) rename assets/{ => data}/img/flags/MH.png (100%) rename assets/{ => data}/img/flags/MK.png (100%) rename assets/{ => data}/img/flags/ML.png (100%) rename assets/{ => data}/img/flags/MM.png (100%) rename assets/{ => data}/img/flags/MN.png (100%) rename assets/{ => data}/img/flags/MO.png (100%) rename assets/{ => data}/img/flags/MP.png (100%) rename assets/{ => data}/img/flags/MQ.png (100%) rename assets/{ => data}/img/flags/MR.png (100%) rename assets/{ => data}/img/flags/MS.png (100%) rename assets/{ => data}/img/flags/MT.png (100%) rename assets/{ => data}/img/flags/MU.png (100%) rename assets/{ => data}/img/flags/MV.png (100%) rename assets/{ => data}/img/flags/MW.png (100%) rename assets/{ => data}/img/flags/MX.png (100%) rename assets/{ => data}/img/flags/MY.png (100%) rename assets/{ => data}/img/flags/MZ.png (100%) rename assets/{ => data}/img/flags/NA.png (100%) rename assets/{ => data}/img/flags/NC.png (100%) rename assets/{ => data}/img/flags/NE.png (100%) rename assets/{ => data}/img/flags/NF.png (100%) rename assets/{ => data}/img/flags/NG.png (100%) rename assets/{ => data}/img/flags/NI.png (100%) rename assets/{ => data}/img/flags/NL.png (100%) rename assets/{ => data}/img/flags/NO.png (100%) rename assets/{ => data}/img/flags/NP.png (100%) rename assets/{ => data}/img/flags/NR.png (100%) rename assets/{ => data}/img/flags/NU.png (100%) rename assets/{ => data}/img/flags/NZ.png (100%) rename assets/{ => data}/img/flags/OM.png (100%) rename assets/{ => data}/img/flags/PA.png (100%) rename assets/{ => data}/img/flags/PE.png (100%) rename assets/{ => data}/img/flags/PF.png (100%) rename assets/{ => data}/img/flags/PG.png (100%) rename assets/{ => data}/img/flags/PH.png (100%) rename assets/{ => data}/img/flags/PK.png (100%) rename assets/{ => data}/img/flags/PL.png (100%) rename assets/{ => data}/img/flags/PN.png (100%) rename assets/{ => data}/img/flags/PR.png (100%) rename assets/{ => data}/img/flags/PS.png (100%) rename assets/{ => data}/img/flags/PT.png (100%) rename assets/{ => data}/img/flags/PW.png (100%) rename assets/{ => data}/img/flags/PY.png (100%) rename assets/{ => data}/img/flags/QA.png (100%) rename assets/{ => data}/img/flags/RE.png (100%) rename assets/{ => data}/img/flags/RO.png (100%) rename assets/{ => data}/img/flags/RS.png (100%) rename assets/{ => data}/img/flags/RU.png (100%) rename assets/{ => data}/img/flags/RW.png (100%) rename assets/{ => data}/img/flags/SA.png (100%) rename assets/{ => data}/img/flags/SB.png (100%) rename assets/{ => data}/img/flags/SC.png (100%) rename assets/{ => data}/img/flags/SD.png (100%) rename assets/{ => data}/img/flags/SE.png (100%) rename assets/{ => data}/img/flags/SG.png (100%) rename assets/{ => data}/img/flags/SH.png (100%) rename assets/{ => data}/img/flags/SI.png (100%) rename assets/{ => data}/img/flags/SK.png (100%) rename assets/{ => data}/img/flags/SL.png (100%) rename assets/{ => data}/img/flags/SM.png (100%) rename assets/{ => data}/img/flags/SN.png (100%) rename assets/{ => data}/img/flags/SO.png (100%) rename assets/{ => data}/img/flags/SR.png (100%) rename assets/{ => data}/img/flags/SS.png (100%) rename assets/{ => data}/img/flags/ST.png (100%) rename assets/{ => data}/img/flags/SV.png (100%) rename assets/{ => data}/img/flags/SX.png (100%) rename assets/{ => data}/img/flags/SY.png (100%) rename assets/{ => data}/img/flags/SZ.png (100%) rename assets/{ => data}/img/flags/TC.png (100%) rename assets/{ => data}/img/flags/TD.png (100%) rename assets/{ => data}/img/flags/TF.png (100%) rename assets/{ => data}/img/flags/TG.png (100%) rename assets/{ => data}/img/flags/TH.png (100%) rename assets/{ => data}/img/flags/TJ.png (100%) rename assets/{ => data}/img/flags/TK.png (100%) rename assets/{ => data}/img/flags/TL.png (100%) rename assets/{ => data}/img/flags/TM.png (100%) rename assets/{ => data}/img/flags/TN.png (100%) rename assets/{ => data}/img/flags/TO.png (100%) rename assets/{ => data}/img/flags/TR.png (100%) rename assets/{ => data}/img/flags/TT.png (100%) rename assets/{ => data}/img/flags/TV.png (100%) rename assets/{ => data}/img/flags/TW.png (100%) rename assets/{ => data}/img/flags/TZ.png (100%) rename assets/{ => data}/img/flags/UA.png (100%) rename assets/{ => data}/img/flags/UG.png (100%) rename assets/{ => data}/img/flags/US.png (100%) rename assets/{ => data}/img/flags/UY.png (100%) rename assets/{ => data}/img/flags/UZ.png (100%) rename assets/{ => data}/img/flags/VA.png (100%) rename assets/{ => data}/img/flags/VC.png (100%) rename assets/{ => data}/img/flags/VE.png (100%) rename assets/{ => data}/img/flags/VG.png (100%) rename assets/{ => data}/img/flags/VI.png (100%) rename assets/{ => data}/img/flags/VN.png (100%) rename assets/{ => data}/img/flags/VU.png (100%) rename assets/{ => data}/img/flags/WF.png (100%) rename assets/{ => data}/img/flags/WS.png (100%) rename assets/{ => data}/img/flags/YE.png (100%) rename assets/{ => data}/img/flags/YT.png (100%) rename assets/{ => data}/img/flags/ZA.png (100%) rename assets/{ => data}/img/flags/ZM.png (100%) rename assets/{ => data}/img/flags/ZW.png (100%) rename assets/{ => data}/img/flags/_abkhazia.png (100%) rename assets/{ => data}/img/flags/_basque-country.png (100%) rename assets/{ => data}/img/flags/_british-antarctic-territory.png (100%) rename assets/{ => data}/img/flags/_commonwealth.png (100%) rename assets/{ => data}/img/flags/_england.png (100%) rename assets/{ => data}/img/flags/_gosquared.png (100%) rename assets/{ => data}/img/flags/_kosovo.png (100%) rename assets/{ => data}/img/flags/_mars.png (100%) rename assets/{ => data}/img/flags/_nagorno-karabakh.png (100%) rename assets/{ => data}/img/flags/_nato.png (100%) rename assets/{ => data}/img/flags/_northern-cyprus.png (100%) rename assets/{ => data}/img/flags/_olympics.png (100%) rename assets/{ => data}/img/flags/_red-cross.png (100%) rename assets/{ => data}/img/flags/_scotland.png (100%) rename assets/{ => data}/img/flags/_somaliland.png (100%) rename assets/{ => data}/img/flags/_south-ossetia.png (100%) rename assets/{ => data}/img/flags/_united-nations.png (100%) rename assets/{ => data}/img/flags/_unknown.png (100%) rename assets/{ => data}/img/flags/_wales.png (100%) rename assets/{ => data}/img/linux.svg (100%) rename assets/{ => data}/img/mac.svg (100%) rename assets/{ => data}/img/plants1-br.png (100%) rename assets/{ => data}/img/plants1.png (100%) rename assets/{ => data}/img/spn-feature-carousel/access-regional-content-easily.png (100%) rename assets/{ => data}/img/spn-feature-carousel/built-from-the-ground-up.png (100%) rename assets/{ => data}/img/spn-feature-carousel/bye-bye-vpns.png (100%) rename assets/{ => data}/img/spn-feature-carousel/easily-control-your-privacy.png (100%) rename assets/{ => data}/img/spn-feature-carousel/multiple-identities-for-each-app.png (100%) rename assets/{ => data}/img/spn-login.png (100%) rename assets/{ => data}/img/windows.svg (100%) rename assets/{ => data}/world-50m.json (100%) create mode 100644 assets/icons.go create mode 100644 assets/icons_default.go create mode 100644 assets/icons_windows.go create mode 100644 cmds/notifier/.gitignore create mode 100644 cmds/notifier/README.md create mode 100644 cmds/notifier/http_api.go create mode 100644 cmds/notifier/icons.go create mode 100644 cmds/notifier/main.go create mode 100644 cmds/notifier/notification.go create mode 100644 cmds/notifier/notify.go create mode 100644 cmds/notifier/notify_linux.go create mode 100644 cmds/notifier/notify_windows.go create mode 100644 cmds/notifier/shutdown.go create mode 100644 cmds/notifier/snoretoast-guid.patch create mode 100644 cmds/notifier/spn.go create mode 100644 cmds/notifier/subsystems.go create mode 100644 cmds/notifier/tray.go create mode 100644 cmds/notifier/wintoast/notification_builder.go create mode 100644 cmds/notifier/wintoast/wintoast.go delete mode 100644 runtime/.gitkeep diff --git a/Earthfile b/Earthfile index 3638f0d6..c0da3562 100644 --- a/Earthfile +++ b/Earthfile @@ -35,6 +35,13 @@ go-base: COPY service ./service COPY spn ./spn + # The cmds/notifier embeds some icons but go:embed is not allowed + # to leave the package directory so there's a small go-package in + # assets. Once we drop the notify in favor of the tauri replacement + # we can remove the following line and also remove all go-code from + # ./assets + COPY assets ./assets + # mod-tidy runs 'go mod tidy', saving go.mod and go.sum locally. mod-tidy: FROM +go-base @@ -140,7 +147,7 @@ angular-deps: COPY desktop/angular/package.json . COPY desktop/angular/package-lock.json . - COPY assets/ ./assets + COPY assets/data ./assets RUN npm install diff --git a/assets/fonts/Roboto-300/LICENSE.txt b/assets/data/fonts/Roboto-300/LICENSE.txt similarity index 100% rename from assets/fonts/Roboto-300/LICENSE.txt rename to assets/data/fonts/Roboto-300/LICENSE.txt diff --git a/assets/fonts/Roboto-300/Roboto-300.eot b/assets/data/fonts/Roboto-300/Roboto-300.eot similarity index 100% rename from assets/fonts/Roboto-300/Roboto-300.eot rename to assets/data/fonts/Roboto-300/Roboto-300.eot diff --git a/assets/fonts/Roboto-300/Roboto-300.svg b/assets/data/fonts/Roboto-300/Roboto-300.svg similarity index 100% rename from assets/fonts/Roboto-300/Roboto-300.svg rename to assets/data/fonts/Roboto-300/Roboto-300.svg diff --git a/assets/fonts/Roboto-300/Roboto-300.ttf b/assets/data/fonts/Roboto-300/Roboto-300.ttf similarity index 100% rename from assets/fonts/Roboto-300/Roboto-300.ttf rename to assets/data/fonts/Roboto-300/Roboto-300.ttf diff --git a/assets/fonts/Roboto-300/Roboto-300.woff b/assets/data/fonts/Roboto-300/Roboto-300.woff similarity index 100% rename from assets/fonts/Roboto-300/Roboto-300.woff rename to assets/data/fonts/Roboto-300/Roboto-300.woff diff --git a/assets/fonts/Roboto-300/Roboto-300.woff2 b/assets/data/fonts/Roboto-300/Roboto-300.woff2 similarity index 100% rename from assets/fonts/Roboto-300/Roboto-300.woff2 rename to assets/data/fonts/Roboto-300/Roboto-300.woff2 diff --git a/assets/fonts/Roboto-300italic/LICENSE.txt b/assets/data/fonts/Roboto-300italic/LICENSE.txt similarity index 100% rename from assets/fonts/Roboto-300italic/LICENSE.txt rename to assets/data/fonts/Roboto-300italic/LICENSE.txt diff --git a/assets/fonts/Roboto-300italic/Roboto-300italic.eot b/assets/data/fonts/Roboto-300italic/Roboto-300italic.eot similarity index 100% rename from assets/fonts/Roboto-300italic/Roboto-300italic.eot rename to assets/data/fonts/Roboto-300italic/Roboto-300italic.eot diff --git a/assets/fonts/Roboto-300italic/Roboto-300italic.svg b/assets/data/fonts/Roboto-300italic/Roboto-300italic.svg similarity index 100% rename from assets/fonts/Roboto-300italic/Roboto-300italic.svg rename to assets/data/fonts/Roboto-300italic/Roboto-300italic.svg diff --git a/assets/fonts/Roboto-300italic/Roboto-300italic.ttf b/assets/data/fonts/Roboto-300italic/Roboto-300italic.ttf similarity index 100% rename from assets/fonts/Roboto-300italic/Roboto-300italic.ttf rename to assets/data/fonts/Roboto-300italic/Roboto-300italic.ttf diff --git a/assets/fonts/Roboto-300italic/Roboto-300italic.woff b/assets/data/fonts/Roboto-300italic/Roboto-300italic.woff similarity index 100% rename from assets/fonts/Roboto-300italic/Roboto-300italic.woff rename to assets/data/fonts/Roboto-300italic/Roboto-300italic.woff diff --git a/assets/fonts/Roboto-300italic/Roboto-300italic.woff2 b/assets/data/fonts/Roboto-300italic/Roboto-300italic.woff2 similarity index 100% rename from assets/fonts/Roboto-300italic/Roboto-300italic.woff2 rename to assets/data/fonts/Roboto-300italic/Roboto-300italic.woff2 diff --git a/assets/fonts/Roboto-500/LICENSE.txt b/assets/data/fonts/Roboto-500/LICENSE.txt similarity index 100% rename from assets/fonts/Roboto-500/LICENSE.txt rename to assets/data/fonts/Roboto-500/LICENSE.txt diff --git a/assets/fonts/Roboto-500/Roboto-500.eot b/assets/data/fonts/Roboto-500/Roboto-500.eot similarity index 100% rename from assets/fonts/Roboto-500/Roboto-500.eot rename to assets/data/fonts/Roboto-500/Roboto-500.eot diff --git a/assets/fonts/Roboto-500/Roboto-500.svg b/assets/data/fonts/Roboto-500/Roboto-500.svg similarity index 100% rename from assets/fonts/Roboto-500/Roboto-500.svg rename to assets/data/fonts/Roboto-500/Roboto-500.svg diff --git a/assets/fonts/Roboto-500/Roboto-500.ttf b/assets/data/fonts/Roboto-500/Roboto-500.ttf similarity index 100% rename from assets/fonts/Roboto-500/Roboto-500.ttf rename to assets/data/fonts/Roboto-500/Roboto-500.ttf diff --git a/assets/fonts/Roboto-500/Roboto-500.woff b/assets/data/fonts/Roboto-500/Roboto-500.woff similarity index 100% rename from assets/fonts/Roboto-500/Roboto-500.woff rename to assets/data/fonts/Roboto-500/Roboto-500.woff diff --git a/assets/fonts/Roboto-500/Roboto-500.woff2 b/assets/data/fonts/Roboto-500/Roboto-500.woff2 similarity index 100% rename from assets/fonts/Roboto-500/Roboto-500.woff2 rename to assets/data/fonts/Roboto-500/Roboto-500.woff2 diff --git a/assets/fonts/Roboto-500italic/LICENSE.txt b/assets/data/fonts/Roboto-500italic/LICENSE.txt similarity index 100% rename from assets/fonts/Roboto-500italic/LICENSE.txt rename to assets/data/fonts/Roboto-500italic/LICENSE.txt diff --git a/assets/fonts/Roboto-500italic/Roboto-500italic.eot b/assets/data/fonts/Roboto-500italic/Roboto-500italic.eot similarity index 100% rename from assets/fonts/Roboto-500italic/Roboto-500italic.eot rename to assets/data/fonts/Roboto-500italic/Roboto-500italic.eot diff --git a/assets/fonts/Roboto-500italic/Roboto-500italic.svg b/assets/data/fonts/Roboto-500italic/Roboto-500italic.svg similarity index 100% rename from assets/fonts/Roboto-500italic/Roboto-500italic.svg rename to assets/data/fonts/Roboto-500italic/Roboto-500italic.svg diff --git a/assets/fonts/Roboto-500italic/Roboto-500italic.ttf b/assets/data/fonts/Roboto-500italic/Roboto-500italic.ttf similarity index 100% rename from assets/fonts/Roboto-500italic/Roboto-500italic.ttf rename to assets/data/fonts/Roboto-500italic/Roboto-500italic.ttf diff --git a/assets/fonts/Roboto-500italic/Roboto-500italic.woff b/assets/data/fonts/Roboto-500italic/Roboto-500italic.woff similarity index 100% rename from assets/fonts/Roboto-500italic/Roboto-500italic.woff rename to assets/data/fonts/Roboto-500italic/Roboto-500italic.woff diff --git a/assets/fonts/Roboto-500italic/Roboto-500italic.woff2 b/assets/data/fonts/Roboto-500italic/Roboto-500italic.woff2 similarity index 100% rename from assets/fonts/Roboto-500italic/Roboto-500italic.woff2 rename to assets/data/fonts/Roboto-500italic/Roboto-500italic.woff2 diff --git a/assets/fonts/Roboto-700/LICENSE.txt b/assets/data/fonts/Roboto-700/LICENSE.txt similarity index 100% rename from assets/fonts/Roboto-700/LICENSE.txt rename to assets/data/fonts/Roboto-700/LICENSE.txt diff --git a/assets/fonts/Roboto-700/Roboto-700.eot b/assets/data/fonts/Roboto-700/Roboto-700.eot similarity index 100% rename from assets/fonts/Roboto-700/Roboto-700.eot rename to assets/data/fonts/Roboto-700/Roboto-700.eot diff --git a/assets/fonts/Roboto-700/Roboto-700.svg b/assets/data/fonts/Roboto-700/Roboto-700.svg similarity index 100% rename from assets/fonts/Roboto-700/Roboto-700.svg rename to assets/data/fonts/Roboto-700/Roboto-700.svg diff --git a/assets/fonts/Roboto-700/Roboto-700.ttf b/assets/data/fonts/Roboto-700/Roboto-700.ttf similarity index 100% rename from assets/fonts/Roboto-700/Roboto-700.ttf rename to assets/data/fonts/Roboto-700/Roboto-700.ttf diff --git a/assets/fonts/Roboto-700/Roboto-700.woff b/assets/data/fonts/Roboto-700/Roboto-700.woff similarity index 100% rename from assets/fonts/Roboto-700/Roboto-700.woff rename to assets/data/fonts/Roboto-700/Roboto-700.woff diff --git a/assets/fonts/Roboto-700/Roboto-700.woff2 b/assets/data/fonts/Roboto-700/Roboto-700.woff2 similarity index 100% rename from assets/fonts/Roboto-700/Roboto-700.woff2 rename to assets/data/fonts/Roboto-700/Roboto-700.woff2 diff --git a/assets/fonts/Roboto-700italic/LICENSE.txt b/assets/data/fonts/Roboto-700italic/LICENSE.txt similarity index 100% rename from assets/fonts/Roboto-700italic/LICENSE.txt rename to assets/data/fonts/Roboto-700italic/LICENSE.txt diff --git a/assets/fonts/Roboto-700italic/Roboto-700italic.eot b/assets/data/fonts/Roboto-700italic/Roboto-700italic.eot similarity index 100% rename from assets/fonts/Roboto-700italic/Roboto-700italic.eot rename to assets/data/fonts/Roboto-700italic/Roboto-700italic.eot diff --git a/assets/fonts/Roboto-700italic/Roboto-700italic.svg b/assets/data/fonts/Roboto-700italic/Roboto-700italic.svg similarity index 100% rename from assets/fonts/Roboto-700italic/Roboto-700italic.svg rename to assets/data/fonts/Roboto-700italic/Roboto-700italic.svg diff --git a/assets/fonts/Roboto-700italic/Roboto-700italic.ttf b/assets/data/fonts/Roboto-700italic/Roboto-700italic.ttf similarity index 100% rename from assets/fonts/Roboto-700italic/Roboto-700italic.ttf rename to assets/data/fonts/Roboto-700italic/Roboto-700italic.ttf diff --git a/assets/fonts/Roboto-700italic/Roboto-700italic.woff b/assets/data/fonts/Roboto-700italic/Roboto-700italic.woff similarity index 100% rename from assets/fonts/Roboto-700italic/Roboto-700italic.woff rename to assets/data/fonts/Roboto-700italic/Roboto-700italic.woff diff --git a/assets/fonts/Roboto-700italic/Roboto-700italic.woff2 b/assets/data/fonts/Roboto-700italic/Roboto-700italic.woff2 similarity index 100% rename from assets/fonts/Roboto-700italic/Roboto-700italic.woff2 rename to assets/data/fonts/Roboto-700italic/Roboto-700italic.woff2 diff --git a/assets/fonts/Roboto-italic/LICENSE.txt b/assets/data/fonts/Roboto-italic/LICENSE.txt similarity index 100% rename from assets/fonts/Roboto-italic/LICENSE.txt rename to assets/data/fonts/Roboto-italic/LICENSE.txt diff --git a/assets/fonts/Roboto-italic/Roboto-italic.eot b/assets/data/fonts/Roboto-italic/Roboto-italic.eot similarity index 100% rename from assets/fonts/Roboto-italic/Roboto-italic.eot rename to assets/data/fonts/Roboto-italic/Roboto-italic.eot diff --git a/assets/fonts/Roboto-italic/Roboto-italic.svg b/assets/data/fonts/Roboto-italic/Roboto-italic.svg similarity index 100% rename from assets/fonts/Roboto-italic/Roboto-italic.svg rename to assets/data/fonts/Roboto-italic/Roboto-italic.svg diff --git a/assets/fonts/Roboto-italic/Roboto-italic.ttf b/assets/data/fonts/Roboto-italic/Roboto-italic.ttf similarity index 100% rename from assets/fonts/Roboto-italic/Roboto-italic.ttf rename to assets/data/fonts/Roboto-italic/Roboto-italic.ttf diff --git a/assets/fonts/Roboto-italic/Roboto-italic.woff b/assets/data/fonts/Roboto-italic/Roboto-italic.woff similarity index 100% rename from assets/fonts/Roboto-italic/Roboto-italic.woff rename to assets/data/fonts/Roboto-italic/Roboto-italic.woff diff --git a/assets/fonts/Roboto-italic/Roboto-italic.woff2 b/assets/data/fonts/Roboto-italic/Roboto-italic.woff2 similarity index 100% rename from assets/fonts/Roboto-italic/Roboto-italic.woff2 rename to assets/data/fonts/Roboto-italic/Roboto-italic.woff2 diff --git a/assets/fonts/Roboto-regular/LICENSE.txt b/assets/data/fonts/Roboto-regular/LICENSE.txt similarity index 100% rename from assets/fonts/Roboto-regular/LICENSE.txt rename to assets/data/fonts/Roboto-regular/LICENSE.txt diff --git a/assets/fonts/Roboto-regular/Roboto-regular.eot b/assets/data/fonts/Roboto-regular/Roboto-regular.eot similarity index 100% rename from assets/fonts/Roboto-regular/Roboto-regular.eot rename to assets/data/fonts/Roboto-regular/Roboto-regular.eot diff --git a/assets/fonts/Roboto-regular/Roboto-regular.svg b/assets/data/fonts/Roboto-regular/Roboto-regular.svg similarity index 100% rename from assets/fonts/Roboto-regular/Roboto-regular.svg rename to assets/data/fonts/Roboto-regular/Roboto-regular.svg diff --git a/assets/fonts/Roboto-regular/Roboto-regular.ttf b/assets/data/fonts/Roboto-regular/Roboto-regular.ttf similarity index 100% rename from assets/fonts/Roboto-regular/Roboto-regular.ttf rename to assets/data/fonts/Roboto-regular/Roboto-regular.ttf diff --git a/assets/fonts/Roboto-regular/Roboto-regular.woff b/assets/data/fonts/Roboto-regular/Roboto-regular.woff similarity index 100% rename from assets/fonts/Roboto-regular/Roboto-regular.woff rename to assets/data/fonts/Roboto-regular/Roboto-regular.woff diff --git a/assets/fonts/Roboto-regular/Roboto-regular.woff2 b/assets/data/fonts/Roboto-regular/Roboto-regular.woff2 similarity index 100% rename from assets/fonts/Roboto-regular/Roboto-regular.woff2 rename to assets/data/fonts/Roboto-regular/Roboto-regular.woff2 diff --git a/assets/fonts/roboto-slimfix.css b/assets/data/fonts/roboto-slimfix.css similarity index 100% rename from assets/fonts/roboto-slimfix.css rename to assets/data/fonts/roboto-slimfix.css diff --git a/assets/fonts/roboto.css b/assets/data/fonts/roboto.css similarity index 100% rename from assets/fonts/roboto.css rename to assets/data/fonts/roboto.css diff --git a/assets/icons/README.md b/assets/data/icons/README.md similarity index 100% rename from assets/icons/README.md rename to assets/data/icons/README.md diff --git a/assets/icons/pm_dark_128.png b/assets/data/icons/pm_dark_128.png similarity index 100% rename from assets/icons/pm_dark_128.png rename to assets/data/icons/pm_dark_128.png diff --git a/assets/icons/pm_dark_256.png b/assets/data/icons/pm_dark_256.png similarity index 100% rename from assets/icons/pm_dark_256.png rename to assets/data/icons/pm_dark_256.png diff --git a/assets/icons/pm_dark_512.ico b/assets/data/icons/pm_dark_512.ico similarity index 100% rename from assets/icons/pm_dark_512.ico rename to assets/data/icons/pm_dark_512.ico diff --git a/assets/icons/pm_dark_512.png b/assets/data/icons/pm_dark_512.png similarity index 100% rename from assets/icons/pm_dark_512.png rename to assets/data/icons/pm_dark_512.png diff --git a/assets/icons/pm_dark_blue_128.png b/assets/data/icons/pm_dark_blue_128.png similarity index 100% rename from assets/icons/pm_dark_blue_128.png rename to assets/data/icons/pm_dark_blue_128.png diff --git a/assets/icons/pm_dark_blue_256.png b/assets/data/icons/pm_dark_blue_256.png similarity index 100% rename from assets/icons/pm_dark_blue_256.png rename to assets/data/icons/pm_dark_blue_256.png diff --git a/assets/icons/pm_dark_blue_512.ico b/assets/data/icons/pm_dark_blue_512.ico similarity index 100% rename from assets/icons/pm_dark_blue_512.ico rename to assets/data/icons/pm_dark_blue_512.ico diff --git a/assets/icons/pm_dark_blue_512.png b/assets/data/icons/pm_dark_blue_512.png similarity index 100% rename from assets/icons/pm_dark_blue_512.png rename to assets/data/icons/pm_dark_blue_512.png diff --git a/assets/icons/pm_dark_green_128.png b/assets/data/icons/pm_dark_green_128.png similarity index 100% rename from assets/icons/pm_dark_green_128.png rename to assets/data/icons/pm_dark_green_128.png diff --git a/assets/icons/pm_dark_green_256.png b/assets/data/icons/pm_dark_green_256.png similarity index 100% rename from assets/icons/pm_dark_green_256.png rename to assets/data/icons/pm_dark_green_256.png diff --git a/assets/icons/pm_dark_green_512.ico b/assets/data/icons/pm_dark_green_512.ico similarity index 100% rename from assets/icons/pm_dark_green_512.ico rename to assets/data/icons/pm_dark_green_512.ico diff --git a/assets/icons/pm_dark_green_512.png b/assets/data/icons/pm_dark_green_512.png similarity index 100% rename from assets/icons/pm_dark_green_512.png rename to assets/data/icons/pm_dark_green_512.png diff --git a/assets/icons/pm_dark_red_128.png b/assets/data/icons/pm_dark_red_128.png similarity index 100% rename from assets/icons/pm_dark_red_128.png rename to assets/data/icons/pm_dark_red_128.png diff --git a/assets/icons/pm_dark_red_256.png b/assets/data/icons/pm_dark_red_256.png similarity index 100% rename from assets/icons/pm_dark_red_256.png rename to assets/data/icons/pm_dark_red_256.png diff --git a/assets/icons/pm_dark_red_512.ico b/assets/data/icons/pm_dark_red_512.ico similarity index 100% rename from assets/icons/pm_dark_red_512.ico rename to assets/data/icons/pm_dark_red_512.ico diff --git a/assets/icons/pm_dark_red_512.png b/assets/data/icons/pm_dark_red_512.png similarity index 100% rename from assets/icons/pm_dark_red_512.png rename to assets/data/icons/pm_dark_red_512.png diff --git a/assets/icons/pm_dark_yellow_128.png b/assets/data/icons/pm_dark_yellow_128.png similarity index 100% rename from assets/icons/pm_dark_yellow_128.png rename to assets/data/icons/pm_dark_yellow_128.png diff --git a/assets/icons/pm_dark_yellow_256.png b/assets/data/icons/pm_dark_yellow_256.png similarity index 100% rename from assets/icons/pm_dark_yellow_256.png rename to assets/data/icons/pm_dark_yellow_256.png diff --git a/assets/icons/pm_dark_yellow_512.ico b/assets/data/icons/pm_dark_yellow_512.ico similarity index 100% rename from assets/icons/pm_dark_yellow_512.ico rename to assets/data/icons/pm_dark_yellow_512.ico diff --git a/assets/icons/pm_dark_yellow_512.png b/assets/data/icons/pm_dark_yellow_512.png similarity index 100% rename from assets/icons/pm_dark_yellow_512.png rename to assets/data/icons/pm_dark_yellow_512.png diff --git a/assets/icons/pm_light_128.png b/assets/data/icons/pm_light_128.png similarity index 100% rename from assets/icons/pm_light_128.png rename to assets/data/icons/pm_light_128.png diff --git a/assets/icons/pm_light_256.png b/assets/data/icons/pm_light_256.png similarity index 100% rename from assets/icons/pm_light_256.png rename to assets/data/icons/pm_light_256.png diff --git a/assets/icons/pm_light_512.ico b/assets/data/icons/pm_light_512.ico similarity index 100% rename from assets/icons/pm_light_512.ico rename to assets/data/icons/pm_light_512.ico diff --git a/assets/icons/pm_light_512.png b/assets/data/icons/pm_light_512.png similarity index 100% rename from assets/icons/pm_light_512.png rename to assets/data/icons/pm_light_512.png diff --git a/assets/icons/pm_light_blue_128.png b/assets/data/icons/pm_light_blue_128.png similarity index 100% rename from assets/icons/pm_light_blue_128.png rename to assets/data/icons/pm_light_blue_128.png diff --git a/assets/icons/pm_light_blue_256.png b/assets/data/icons/pm_light_blue_256.png similarity index 100% rename from assets/icons/pm_light_blue_256.png rename to assets/data/icons/pm_light_blue_256.png diff --git a/assets/icons/pm_light_blue_512.ico b/assets/data/icons/pm_light_blue_512.ico similarity index 100% rename from assets/icons/pm_light_blue_512.ico rename to assets/data/icons/pm_light_blue_512.ico diff --git a/assets/icons/pm_light_blue_512.png b/assets/data/icons/pm_light_blue_512.png similarity index 100% rename from assets/icons/pm_light_blue_512.png rename to assets/data/icons/pm_light_blue_512.png diff --git a/assets/icons/pm_light_green_128.png b/assets/data/icons/pm_light_green_128.png similarity index 100% rename from assets/icons/pm_light_green_128.png rename to assets/data/icons/pm_light_green_128.png diff --git a/assets/icons/pm_light_green_256.png b/assets/data/icons/pm_light_green_256.png similarity index 100% rename from assets/icons/pm_light_green_256.png rename to assets/data/icons/pm_light_green_256.png diff --git a/assets/icons/pm_light_green_512.ico b/assets/data/icons/pm_light_green_512.ico similarity index 100% rename from assets/icons/pm_light_green_512.ico rename to assets/data/icons/pm_light_green_512.ico diff --git a/assets/icons/pm_light_green_512.png b/assets/data/icons/pm_light_green_512.png similarity index 100% rename from assets/icons/pm_light_green_512.png rename to assets/data/icons/pm_light_green_512.png diff --git a/assets/icons/pm_light_red_128.png b/assets/data/icons/pm_light_red_128.png similarity index 100% rename from assets/icons/pm_light_red_128.png rename to assets/data/icons/pm_light_red_128.png diff --git a/assets/icons/pm_light_red_256.png b/assets/data/icons/pm_light_red_256.png similarity index 100% rename from assets/icons/pm_light_red_256.png rename to assets/data/icons/pm_light_red_256.png diff --git a/assets/icons/pm_light_red_512.ico b/assets/data/icons/pm_light_red_512.ico similarity index 100% rename from assets/icons/pm_light_red_512.ico rename to assets/data/icons/pm_light_red_512.ico diff --git a/assets/icons/pm_light_red_512.png b/assets/data/icons/pm_light_red_512.png similarity index 100% rename from assets/icons/pm_light_red_512.png rename to assets/data/icons/pm_light_red_512.png diff --git a/assets/icons/pm_light_yellow_128.png b/assets/data/icons/pm_light_yellow_128.png similarity index 100% rename from assets/icons/pm_light_yellow_128.png rename to assets/data/icons/pm_light_yellow_128.png diff --git a/assets/icons/pm_light_yellow_256.png b/assets/data/icons/pm_light_yellow_256.png similarity index 100% rename from assets/icons/pm_light_yellow_256.png rename to assets/data/icons/pm_light_yellow_256.png diff --git a/assets/icons/pm_light_yellow_512.ico b/assets/data/icons/pm_light_yellow_512.ico similarity index 100% rename from assets/icons/pm_light_yellow_512.ico rename to assets/data/icons/pm_light_yellow_512.ico diff --git a/assets/icons/pm_light_yellow_512.png b/assets/data/icons/pm_light_yellow_512.png similarity index 100% rename from assets/icons/pm_light_yellow_512.png rename to assets/data/icons/pm_light_yellow_512.png diff --git a/assets/img/Mobile.svg b/assets/data/img/Mobile.svg similarity index 100% rename from assets/img/Mobile.svg rename to assets/data/img/Mobile.svg diff --git a/assets/img/flags/AD.png b/assets/data/img/flags/AD.png similarity index 100% rename from assets/img/flags/AD.png rename to assets/data/img/flags/AD.png diff --git a/assets/img/flags/AE.png b/assets/data/img/flags/AE.png similarity index 100% rename from assets/img/flags/AE.png rename to assets/data/img/flags/AE.png diff --git a/assets/img/flags/AF.png b/assets/data/img/flags/AF.png similarity index 100% rename from assets/img/flags/AF.png rename to assets/data/img/flags/AF.png diff --git a/assets/img/flags/AG.png b/assets/data/img/flags/AG.png similarity index 100% rename from assets/img/flags/AG.png rename to assets/data/img/flags/AG.png diff --git a/assets/img/flags/AI.png b/assets/data/img/flags/AI.png similarity index 100% rename from assets/img/flags/AI.png rename to assets/data/img/flags/AI.png diff --git a/assets/img/flags/AL.png b/assets/data/img/flags/AL.png similarity index 100% rename from assets/img/flags/AL.png rename to assets/data/img/flags/AL.png diff --git a/assets/img/flags/AM.png b/assets/data/img/flags/AM.png similarity index 100% rename from assets/img/flags/AM.png rename to assets/data/img/flags/AM.png diff --git a/assets/img/flags/AN.png b/assets/data/img/flags/AN.png similarity index 100% rename from assets/img/flags/AN.png rename to assets/data/img/flags/AN.png diff --git a/assets/img/flags/AO.png b/assets/data/img/flags/AO.png similarity index 100% rename from assets/img/flags/AO.png rename to assets/data/img/flags/AO.png diff --git a/assets/img/flags/AQ.png b/assets/data/img/flags/AQ.png similarity index 100% rename from assets/img/flags/AQ.png rename to assets/data/img/flags/AQ.png diff --git a/assets/img/flags/AR.png b/assets/data/img/flags/AR.png similarity index 100% rename from assets/img/flags/AR.png rename to assets/data/img/flags/AR.png diff --git a/assets/img/flags/AS.png b/assets/data/img/flags/AS.png similarity index 100% rename from assets/img/flags/AS.png rename to assets/data/img/flags/AS.png diff --git a/assets/img/flags/AT.png b/assets/data/img/flags/AT.png similarity index 100% rename from assets/img/flags/AT.png rename to assets/data/img/flags/AT.png diff --git a/assets/img/flags/AU.png b/assets/data/img/flags/AU.png similarity index 100% rename from assets/img/flags/AU.png rename to assets/data/img/flags/AU.png diff --git a/assets/img/flags/AW.png b/assets/data/img/flags/AW.png similarity index 100% rename from assets/img/flags/AW.png rename to assets/data/img/flags/AW.png diff --git a/assets/img/flags/AX.png b/assets/data/img/flags/AX.png similarity index 100% rename from assets/img/flags/AX.png rename to assets/data/img/flags/AX.png diff --git a/assets/img/flags/AZ.png b/assets/data/img/flags/AZ.png similarity index 100% rename from assets/img/flags/AZ.png rename to assets/data/img/flags/AZ.png diff --git a/assets/img/flags/BA.png b/assets/data/img/flags/BA.png similarity index 100% rename from assets/img/flags/BA.png rename to assets/data/img/flags/BA.png diff --git a/assets/img/flags/BB.png b/assets/data/img/flags/BB.png similarity index 100% rename from assets/img/flags/BB.png rename to assets/data/img/flags/BB.png diff --git a/assets/img/flags/BD.png b/assets/data/img/flags/BD.png similarity index 100% rename from assets/img/flags/BD.png rename to assets/data/img/flags/BD.png diff --git a/assets/img/flags/BE.png b/assets/data/img/flags/BE.png similarity index 100% rename from assets/img/flags/BE.png rename to assets/data/img/flags/BE.png diff --git a/assets/img/flags/BF.png b/assets/data/img/flags/BF.png similarity index 100% rename from assets/img/flags/BF.png rename to assets/data/img/flags/BF.png diff --git a/assets/img/flags/BG.png b/assets/data/img/flags/BG.png similarity index 100% rename from assets/img/flags/BG.png rename to assets/data/img/flags/BG.png diff --git a/assets/img/flags/BH.png b/assets/data/img/flags/BH.png similarity index 100% rename from assets/img/flags/BH.png rename to assets/data/img/flags/BH.png diff --git a/assets/img/flags/BI.png b/assets/data/img/flags/BI.png similarity index 100% rename from assets/img/flags/BI.png rename to assets/data/img/flags/BI.png diff --git a/assets/img/flags/BJ.png b/assets/data/img/flags/BJ.png similarity index 100% rename from assets/img/flags/BJ.png rename to assets/data/img/flags/BJ.png diff --git a/assets/img/flags/BL.png b/assets/data/img/flags/BL.png similarity index 100% rename from assets/img/flags/BL.png rename to assets/data/img/flags/BL.png diff --git a/assets/img/flags/BM.png b/assets/data/img/flags/BM.png similarity index 100% rename from assets/img/flags/BM.png rename to assets/data/img/flags/BM.png diff --git a/assets/img/flags/BN.png b/assets/data/img/flags/BN.png similarity index 100% rename from assets/img/flags/BN.png rename to assets/data/img/flags/BN.png diff --git a/assets/img/flags/BO.png b/assets/data/img/flags/BO.png similarity index 100% rename from assets/img/flags/BO.png rename to assets/data/img/flags/BO.png diff --git a/assets/img/flags/BR.png b/assets/data/img/flags/BR.png similarity index 100% rename from assets/img/flags/BR.png rename to assets/data/img/flags/BR.png diff --git a/assets/img/flags/BS.png b/assets/data/img/flags/BS.png similarity index 100% rename from assets/img/flags/BS.png rename to assets/data/img/flags/BS.png diff --git a/assets/img/flags/BT.png b/assets/data/img/flags/BT.png similarity index 100% rename from assets/img/flags/BT.png rename to assets/data/img/flags/BT.png diff --git a/assets/img/flags/BW.png b/assets/data/img/flags/BW.png similarity index 100% rename from assets/img/flags/BW.png rename to assets/data/img/flags/BW.png diff --git a/assets/img/flags/BY.png b/assets/data/img/flags/BY.png similarity index 100% rename from assets/img/flags/BY.png rename to assets/data/img/flags/BY.png diff --git a/assets/img/flags/BZ.png b/assets/data/img/flags/BZ.png similarity index 100% rename from assets/img/flags/BZ.png rename to assets/data/img/flags/BZ.png diff --git a/assets/img/flags/CA.png b/assets/data/img/flags/CA.png similarity index 100% rename from assets/img/flags/CA.png rename to assets/data/img/flags/CA.png diff --git a/assets/img/flags/CC.png b/assets/data/img/flags/CC.png similarity index 100% rename from assets/img/flags/CC.png rename to assets/data/img/flags/CC.png diff --git a/assets/img/flags/CD.png b/assets/data/img/flags/CD.png similarity index 100% rename from assets/img/flags/CD.png rename to assets/data/img/flags/CD.png diff --git a/assets/img/flags/CF.png b/assets/data/img/flags/CF.png similarity index 100% rename from assets/img/flags/CF.png rename to assets/data/img/flags/CF.png diff --git a/assets/img/flags/CG.png b/assets/data/img/flags/CG.png similarity index 100% rename from assets/img/flags/CG.png rename to assets/data/img/flags/CG.png diff --git a/assets/img/flags/CH.png b/assets/data/img/flags/CH.png similarity index 100% rename from assets/img/flags/CH.png rename to assets/data/img/flags/CH.png diff --git a/assets/img/flags/CI.png b/assets/data/img/flags/CI.png similarity index 100% rename from assets/img/flags/CI.png rename to assets/data/img/flags/CI.png diff --git a/assets/img/flags/CK.png b/assets/data/img/flags/CK.png similarity index 100% rename from assets/img/flags/CK.png rename to assets/data/img/flags/CK.png diff --git a/assets/img/flags/CL.png b/assets/data/img/flags/CL.png similarity index 100% rename from assets/img/flags/CL.png rename to assets/data/img/flags/CL.png diff --git a/assets/img/flags/CM.png b/assets/data/img/flags/CM.png similarity index 100% rename from assets/img/flags/CM.png rename to assets/data/img/flags/CM.png diff --git a/assets/img/flags/CN.png b/assets/data/img/flags/CN.png similarity index 100% rename from assets/img/flags/CN.png rename to assets/data/img/flags/CN.png diff --git a/assets/img/flags/CO.png b/assets/data/img/flags/CO.png similarity index 100% rename from assets/img/flags/CO.png rename to assets/data/img/flags/CO.png diff --git a/assets/img/flags/CR.png b/assets/data/img/flags/CR.png similarity index 100% rename from assets/img/flags/CR.png rename to assets/data/img/flags/CR.png diff --git a/assets/img/flags/CT.png b/assets/data/img/flags/CT.png similarity index 100% rename from assets/img/flags/CT.png rename to assets/data/img/flags/CT.png diff --git a/assets/img/flags/CU.png b/assets/data/img/flags/CU.png similarity index 100% rename from assets/img/flags/CU.png rename to assets/data/img/flags/CU.png diff --git a/assets/img/flags/CV.png b/assets/data/img/flags/CV.png similarity index 100% rename from assets/img/flags/CV.png rename to assets/data/img/flags/CV.png diff --git a/assets/img/flags/CW.png b/assets/data/img/flags/CW.png similarity index 100% rename from assets/img/flags/CW.png rename to assets/data/img/flags/CW.png diff --git a/assets/img/flags/CX.png b/assets/data/img/flags/CX.png similarity index 100% rename from assets/img/flags/CX.png rename to assets/data/img/flags/CX.png diff --git a/assets/img/flags/CY.png b/assets/data/img/flags/CY.png similarity index 100% rename from assets/img/flags/CY.png rename to assets/data/img/flags/CY.png diff --git a/assets/img/flags/CZ.png b/assets/data/img/flags/CZ.png similarity index 100% rename from assets/img/flags/CZ.png rename to assets/data/img/flags/CZ.png diff --git a/assets/img/flags/DE.png b/assets/data/img/flags/DE.png similarity index 100% rename from assets/img/flags/DE.png rename to assets/data/img/flags/DE.png diff --git a/assets/img/flags/DJ.png b/assets/data/img/flags/DJ.png similarity index 100% rename from assets/img/flags/DJ.png rename to assets/data/img/flags/DJ.png diff --git a/assets/img/flags/DK.png b/assets/data/img/flags/DK.png similarity index 100% rename from assets/img/flags/DK.png rename to assets/data/img/flags/DK.png diff --git a/assets/img/flags/DM.png b/assets/data/img/flags/DM.png similarity index 100% rename from assets/img/flags/DM.png rename to assets/data/img/flags/DM.png diff --git a/assets/img/flags/DO.png b/assets/data/img/flags/DO.png similarity index 100% rename from assets/img/flags/DO.png rename to assets/data/img/flags/DO.png diff --git a/assets/img/flags/DZ.png b/assets/data/img/flags/DZ.png similarity index 100% rename from assets/img/flags/DZ.png rename to assets/data/img/flags/DZ.png diff --git a/assets/img/flags/EC.png b/assets/data/img/flags/EC.png similarity index 100% rename from assets/img/flags/EC.png rename to assets/data/img/flags/EC.png diff --git a/assets/img/flags/EE.png b/assets/data/img/flags/EE.png similarity index 100% rename from assets/img/flags/EE.png rename to assets/data/img/flags/EE.png diff --git a/assets/img/flags/EG.png b/assets/data/img/flags/EG.png similarity index 100% rename from assets/img/flags/EG.png rename to assets/data/img/flags/EG.png diff --git a/assets/img/flags/EH.png b/assets/data/img/flags/EH.png similarity index 100% rename from assets/img/flags/EH.png rename to assets/data/img/flags/EH.png diff --git a/assets/img/flags/ER.png b/assets/data/img/flags/ER.png similarity index 100% rename from assets/img/flags/ER.png rename to assets/data/img/flags/ER.png diff --git a/assets/img/flags/ES.png b/assets/data/img/flags/ES.png similarity index 100% rename from assets/img/flags/ES.png rename to assets/data/img/flags/ES.png diff --git a/assets/img/flags/ET.png b/assets/data/img/flags/ET.png similarity index 100% rename from assets/img/flags/ET.png rename to assets/data/img/flags/ET.png diff --git a/assets/img/flags/EU.png b/assets/data/img/flags/EU.png similarity index 100% rename from assets/img/flags/EU.png rename to assets/data/img/flags/EU.png diff --git a/assets/img/flags/FI.png b/assets/data/img/flags/FI.png similarity index 100% rename from assets/img/flags/FI.png rename to assets/data/img/flags/FI.png diff --git a/assets/img/flags/FJ.png b/assets/data/img/flags/FJ.png similarity index 100% rename from assets/img/flags/FJ.png rename to assets/data/img/flags/FJ.png diff --git a/assets/img/flags/FK.png b/assets/data/img/flags/FK.png similarity index 100% rename from assets/img/flags/FK.png rename to assets/data/img/flags/FK.png diff --git a/assets/img/flags/FM.png b/assets/data/img/flags/FM.png similarity index 100% rename from assets/img/flags/FM.png rename to assets/data/img/flags/FM.png diff --git a/assets/img/flags/FO.png b/assets/data/img/flags/FO.png similarity index 100% rename from assets/img/flags/FO.png rename to assets/data/img/flags/FO.png diff --git a/assets/img/flags/FR.png b/assets/data/img/flags/FR.png similarity index 100% rename from assets/img/flags/FR.png rename to assets/data/img/flags/FR.png diff --git a/assets/img/flags/GA.png b/assets/data/img/flags/GA.png similarity index 100% rename from assets/img/flags/GA.png rename to assets/data/img/flags/GA.png diff --git a/assets/img/flags/GB.png b/assets/data/img/flags/GB.png similarity index 100% rename from assets/img/flags/GB.png rename to assets/data/img/flags/GB.png diff --git a/assets/img/flags/GD.png b/assets/data/img/flags/GD.png similarity index 100% rename from assets/img/flags/GD.png rename to assets/data/img/flags/GD.png diff --git a/assets/img/flags/GE.png b/assets/data/img/flags/GE.png similarity index 100% rename from assets/img/flags/GE.png rename to assets/data/img/flags/GE.png diff --git a/assets/img/flags/GG.png b/assets/data/img/flags/GG.png similarity index 100% rename from assets/img/flags/GG.png rename to assets/data/img/flags/GG.png diff --git a/assets/img/flags/GH.png b/assets/data/img/flags/GH.png similarity index 100% rename from assets/img/flags/GH.png rename to assets/data/img/flags/GH.png diff --git a/assets/img/flags/GI.png b/assets/data/img/flags/GI.png similarity index 100% rename from assets/img/flags/GI.png rename to assets/data/img/flags/GI.png diff --git a/assets/img/flags/GL.png b/assets/data/img/flags/GL.png similarity index 100% rename from assets/img/flags/GL.png rename to assets/data/img/flags/GL.png diff --git a/assets/img/flags/GM.png b/assets/data/img/flags/GM.png similarity index 100% rename from assets/img/flags/GM.png rename to assets/data/img/flags/GM.png diff --git a/assets/img/flags/GN.png b/assets/data/img/flags/GN.png similarity index 100% rename from assets/img/flags/GN.png rename to assets/data/img/flags/GN.png diff --git a/assets/img/flags/GQ.png b/assets/data/img/flags/GQ.png similarity index 100% rename from assets/img/flags/GQ.png rename to assets/data/img/flags/GQ.png diff --git a/assets/img/flags/GR.png b/assets/data/img/flags/GR.png similarity index 100% rename from assets/img/flags/GR.png rename to assets/data/img/flags/GR.png diff --git a/assets/img/flags/GS.png b/assets/data/img/flags/GS.png similarity index 100% rename from assets/img/flags/GS.png rename to assets/data/img/flags/GS.png diff --git a/assets/img/flags/GT.png b/assets/data/img/flags/GT.png similarity index 100% rename from assets/img/flags/GT.png rename to assets/data/img/flags/GT.png diff --git a/assets/img/flags/GU.png b/assets/data/img/flags/GU.png similarity index 100% rename from assets/img/flags/GU.png rename to assets/data/img/flags/GU.png diff --git a/assets/img/flags/GW.png b/assets/data/img/flags/GW.png similarity index 100% rename from assets/img/flags/GW.png rename to assets/data/img/flags/GW.png diff --git a/assets/img/flags/GY.png b/assets/data/img/flags/GY.png similarity index 100% rename from assets/img/flags/GY.png rename to assets/data/img/flags/GY.png diff --git a/assets/img/flags/HK.png b/assets/data/img/flags/HK.png similarity index 100% rename from assets/img/flags/HK.png rename to assets/data/img/flags/HK.png diff --git a/assets/img/flags/HN.png b/assets/data/img/flags/HN.png similarity index 100% rename from assets/img/flags/HN.png rename to assets/data/img/flags/HN.png diff --git a/assets/img/flags/HR.png b/assets/data/img/flags/HR.png similarity index 100% rename from assets/img/flags/HR.png rename to assets/data/img/flags/HR.png diff --git a/assets/img/flags/HT.png b/assets/data/img/flags/HT.png similarity index 100% rename from assets/img/flags/HT.png rename to assets/data/img/flags/HT.png diff --git a/assets/img/flags/HU.png b/assets/data/img/flags/HU.png similarity index 100% rename from assets/img/flags/HU.png rename to assets/data/img/flags/HU.png diff --git a/assets/img/flags/IC.png b/assets/data/img/flags/IC.png similarity index 100% rename from assets/img/flags/IC.png rename to assets/data/img/flags/IC.png diff --git a/assets/img/flags/ID.png b/assets/data/img/flags/ID.png similarity index 100% rename from assets/img/flags/ID.png rename to assets/data/img/flags/ID.png diff --git a/assets/img/flags/IE.png b/assets/data/img/flags/IE.png similarity index 100% rename from assets/img/flags/IE.png rename to assets/data/img/flags/IE.png diff --git a/assets/img/flags/IL.png b/assets/data/img/flags/IL.png similarity index 100% rename from assets/img/flags/IL.png rename to assets/data/img/flags/IL.png diff --git a/assets/img/flags/IM.png b/assets/data/img/flags/IM.png similarity index 100% rename from assets/img/flags/IM.png rename to assets/data/img/flags/IM.png diff --git a/assets/img/flags/IN.png b/assets/data/img/flags/IN.png similarity index 100% rename from assets/img/flags/IN.png rename to assets/data/img/flags/IN.png diff --git a/assets/img/flags/IQ.png b/assets/data/img/flags/IQ.png similarity index 100% rename from assets/img/flags/IQ.png rename to assets/data/img/flags/IQ.png diff --git a/assets/img/flags/IR.png b/assets/data/img/flags/IR.png similarity index 100% rename from assets/img/flags/IR.png rename to assets/data/img/flags/IR.png diff --git a/assets/img/flags/IS.png b/assets/data/img/flags/IS.png similarity index 100% rename from assets/img/flags/IS.png rename to assets/data/img/flags/IS.png diff --git a/assets/img/flags/IT.png b/assets/data/img/flags/IT.png similarity index 100% rename from assets/img/flags/IT.png rename to assets/data/img/flags/IT.png diff --git a/assets/img/flags/JE.png b/assets/data/img/flags/JE.png similarity index 100% rename from assets/img/flags/JE.png rename to assets/data/img/flags/JE.png diff --git a/assets/img/flags/JM.png b/assets/data/img/flags/JM.png similarity index 100% rename from assets/img/flags/JM.png rename to assets/data/img/flags/JM.png diff --git a/assets/img/flags/JO.png b/assets/data/img/flags/JO.png similarity index 100% rename from assets/img/flags/JO.png rename to assets/data/img/flags/JO.png diff --git a/assets/img/flags/JP.png b/assets/data/img/flags/JP.png similarity index 100% rename from assets/img/flags/JP.png rename to assets/data/img/flags/JP.png diff --git a/assets/img/flags/KE.png b/assets/data/img/flags/KE.png similarity index 100% rename from assets/img/flags/KE.png rename to assets/data/img/flags/KE.png diff --git a/assets/img/flags/KG.png b/assets/data/img/flags/KG.png similarity index 100% rename from assets/img/flags/KG.png rename to assets/data/img/flags/KG.png diff --git a/assets/img/flags/KH.png b/assets/data/img/flags/KH.png similarity index 100% rename from assets/img/flags/KH.png rename to assets/data/img/flags/KH.png diff --git a/assets/img/flags/KI.png b/assets/data/img/flags/KI.png similarity index 100% rename from assets/img/flags/KI.png rename to assets/data/img/flags/KI.png diff --git a/assets/img/flags/KM.png b/assets/data/img/flags/KM.png similarity index 100% rename from assets/img/flags/KM.png rename to assets/data/img/flags/KM.png diff --git a/assets/img/flags/KN.png b/assets/data/img/flags/KN.png similarity index 100% rename from assets/img/flags/KN.png rename to assets/data/img/flags/KN.png diff --git a/assets/img/flags/KP.png b/assets/data/img/flags/KP.png similarity index 100% rename from assets/img/flags/KP.png rename to assets/data/img/flags/KP.png diff --git a/assets/img/flags/KR.png b/assets/data/img/flags/KR.png similarity index 100% rename from assets/img/flags/KR.png rename to assets/data/img/flags/KR.png diff --git a/assets/img/flags/KW.png b/assets/data/img/flags/KW.png similarity index 100% rename from assets/img/flags/KW.png rename to assets/data/img/flags/KW.png diff --git a/assets/img/flags/KY.png b/assets/data/img/flags/KY.png similarity index 100% rename from assets/img/flags/KY.png rename to assets/data/img/flags/KY.png diff --git a/assets/img/flags/KZ.png b/assets/data/img/flags/KZ.png similarity index 100% rename from assets/img/flags/KZ.png rename to assets/data/img/flags/KZ.png diff --git a/assets/img/flags/LA.png b/assets/data/img/flags/LA.png similarity index 100% rename from assets/img/flags/LA.png rename to assets/data/img/flags/LA.png diff --git a/assets/img/flags/LB.png b/assets/data/img/flags/LB.png similarity index 100% rename from assets/img/flags/LB.png rename to assets/data/img/flags/LB.png diff --git a/assets/img/flags/LC.png b/assets/data/img/flags/LC.png similarity index 100% rename from assets/img/flags/LC.png rename to assets/data/img/flags/LC.png diff --git a/assets/img/flags/LI.png b/assets/data/img/flags/LI.png similarity index 100% rename from assets/img/flags/LI.png rename to assets/data/img/flags/LI.png diff --git a/assets/img/flags/LICENSE.txt b/assets/data/img/flags/LICENSE.txt similarity index 100% rename from assets/img/flags/LICENSE.txt rename to assets/data/img/flags/LICENSE.txt diff --git a/assets/img/flags/LK.png b/assets/data/img/flags/LK.png similarity index 100% rename from assets/img/flags/LK.png rename to assets/data/img/flags/LK.png diff --git a/assets/img/flags/LR.png b/assets/data/img/flags/LR.png similarity index 100% rename from assets/img/flags/LR.png rename to assets/data/img/flags/LR.png diff --git a/assets/img/flags/LS.png b/assets/data/img/flags/LS.png similarity index 100% rename from assets/img/flags/LS.png rename to assets/data/img/flags/LS.png diff --git a/assets/img/flags/LT.png b/assets/data/img/flags/LT.png similarity index 100% rename from assets/img/flags/LT.png rename to assets/data/img/flags/LT.png diff --git a/assets/img/flags/LU.png b/assets/data/img/flags/LU.png similarity index 100% rename from assets/img/flags/LU.png rename to assets/data/img/flags/LU.png diff --git a/assets/img/flags/LV.png b/assets/data/img/flags/LV.png similarity index 100% rename from assets/img/flags/LV.png rename to assets/data/img/flags/LV.png diff --git a/assets/img/flags/LY.png b/assets/data/img/flags/LY.png similarity index 100% rename from assets/img/flags/LY.png rename to assets/data/img/flags/LY.png diff --git a/assets/img/flags/MA.png b/assets/data/img/flags/MA.png similarity index 100% rename from assets/img/flags/MA.png rename to assets/data/img/flags/MA.png diff --git a/assets/img/flags/MC.png b/assets/data/img/flags/MC.png similarity index 100% rename from assets/img/flags/MC.png rename to assets/data/img/flags/MC.png diff --git a/assets/img/flags/MD.png b/assets/data/img/flags/MD.png similarity index 100% rename from assets/img/flags/MD.png rename to assets/data/img/flags/MD.png diff --git a/assets/img/flags/ME.png b/assets/data/img/flags/ME.png similarity index 100% rename from assets/img/flags/ME.png rename to assets/data/img/flags/ME.png diff --git a/assets/img/flags/MF.png b/assets/data/img/flags/MF.png similarity index 100% rename from assets/img/flags/MF.png rename to assets/data/img/flags/MF.png diff --git a/assets/img/flags/MG.png b/assets/data/img/flags/MG.png similarity index 100% rename from assets/img/flags/MG.png rename to assets/data/img/flags/MG.png diff --git a/assets/img/flags/MH.png b/assets/data/img/flags/MH.png similarity index 100% rename from assets/img/flags/MH.png rename to assets/data/img/flags/MH.png diff --git a/assets/img/flags/MK.png b/assets/data/img/flags/MK.png similarity index 100% rename from assets/img/flags/MK.png rename to assets/data/img/flags/MK.png diff --git a/assets/img/flags/ML.png b/assets/data/img/flags/ML.png similarity index 100% rename from assets/img/flags/ML.png rename to assets/data/img/flags/ML.png diff --git a/assets/img/flags/MM.png b/assets/data/img/flags/MM.png similarity index 100% rename from assets/img/flags/MM.png rename to assets/data/img/flags/MM.png diff --git a/assets/img/flags/MN.png b/assets/data/img/flags/MN.png similarity index 100% rename from assets/img/flags/MN.png rename to assets/data/img/flags/MN.png diff --git a/assets/img/flags/MO.png b/assets/data/img/flags/MO.png similarity index 100% rename from assets/img/flags/MO.png rename to assets/data/img/flags/MO.png diff --git a/assets/img/flags/MP.png b/assets/data/img/flags/MP.png similarity index 100% rename from assets/img/flags/MP.png rename to assets/data/img/flags/MP.png diff --git a/assets/img/flags/MQ.png b/assets/data/img/flags/MQ.png similarity index 100% rename from assets/img/flags/MQ.png rename to assets/data/img/flags/MQ.png diff --git a/assets/img/flags/MR.png b/assets/data/img/flags/MR.png similarity index 100% rename from assets/img/flags/MR.png rename to assets/data/img/flags/MR.png diff --git a/assets/img/flags/MS.png b/assets/data/img/flags/MS.png similarity index 100% rename from assets/img/flags/MS.png rename to assets/data/img/flags/MS.png diff --git a/assets/img/flags/MT.png b/assets/data/img/flags/MT.png similarity index 100% rename from assets/img/flags/MT.png rename to assets/data/img/flags/MT.png diff --git a/assets/img/flags/MU.png b/assets/data/img/flags/MU.png similarity index 100% rename from assets/img/flags/MU.png rename to assets/data/img/flags/MU.png diff --git a/assets/img/flags/MV.png b/assets/data/img/flags/MV.png similarity index 100% rename from assets/img/flags/MV.png rename to assets/data/img/flags/MV.png diff --git a/assets/img/flags/MW.png b/assets/data/img/flags/MW.png similarity index 100% rename from assets/img/flags/MW.png rename to assets/data/img/flags/MW.png diff --git a/assets/img/flags/MX.png b/assets/data/img/flags/MX.png similarity index 100% rename from assets/img/flags/MX.png rename to assets/data/img/flags/MX.png diff --git a/assets/img/flags/MY.png b/assets/data/img/flags/MY.png similarity index 100% rename from assets/img/flags/MY.png rename to assets/data/img/flags/MY.png diff --git a/assets/img/flags/MZ.png b/assets/data/img/flags/MZ.png similarity index 100% rename from assets/img/flags/MZ.png rename to assets/data/img/flags/MZ.png diff --git a/assets/img/flags/NA.png b/assets/data/img/flags/NA.png similarity index 100% rename from assets/img/flags/NA.png rename to assets/data/img/flags/NA.png diff --git a/assets/img/flags/NC.png b/assets/data/img/flags/NC.png similarity index 100% rename from assets/img/flags/NC.png rename to assets/data/img/flags/NC.png diff --git a/assets/img/flags/NE.png b/assets/data/img/flags/NE.png similarity index 100% rename from assets/img/flags/NE.png rename to assets/data/img/flags/NE.png diff --git a/assets/img/flags/NF.png b/assets/data/img/flags/NF.png similarity index 100% rename from assets/img/flags/NF.png rename to assets/data/img/flags/NF.png diff --git a/assets/img/flags/NG.png b/assets/data/img/flags/NG.png similarity index 100% rename from assets/img/flags/NG.png rename to assets/data/img/flags/NG.png diff --git a/assets/img/flags/NI.png b/assets/data/img/flags/NI.png similarity index 100% rename from assets/img/flags/NI.png rename to assets/data/img/flags/NI.png diff --git a/assets/img/flags/NL.png b/assets/data/img/flags/NL.png similarity index 100% rename from assets/img/flags/NL.png rename to assets/data/img/flags/NL.png diff --git a/assets/img/flags/NO.png b/assets/data/img/flags/NO.png similarity index 100% rename from assets/img/flags/NO.png rename to assets/data/img/flags/NO.png diff --git a/assets/img/flags/NP.png b/assets/data/img/flags/NP.png similarity index 100% rename from assets/img/flags/NP.png rename to assets/data/img/flags/NP.png diff --git a/assets/img/flags/NR.png b/assets/data/img/flags/NR.png similarity index 100% rename from assets/img/flags/NR.png rename to assets/data/img/flags/NR.png diff --git a/assets/img/flags/NU.png b/assets/data/img/flags/NU.png similarity index 100% rename from assets/img/flags/NU.png rename to assets/data/img/flags/NU.png diff --git a/assets/img/flags/NZ.png b/assets/data/img/flags/NZ.png similarity index 100% rename from assets/img/flags/NZ.png rename to assets/data/img/flags/NZ.png diff --git a/assets/img/flags/OM.png b/assets/data/img/flags/OM.png similarity index 100% rename from assets/img/flags/OM.png rename to assets/data/img/flags/OM.png diff --git a/assets/img/flags/PA.png b/assets/data/img/flags/PA.png similarity index 100% rename from assets/img/flags/PA.png rename to assets/data/img/flags/PA.png diff --git a/assets/img/flags/PE.png b/assets/data/img/flags/PE.png similarity index 100% rename from assets/img/flags/PE.png rename to assets/data/img/flags/PE.png diff --git a/assets/img/flags/PF.png b/assets/data/img/flags/PF.png similarity index 100% rename from assets/img/flags/PF.png rename to assets/data/img/flags/PF.png diff --git a/assets/img/flags/PG.png b/assets/data/img/flags/PG.png similarity index 100% rename from assets/img/flags/PG.png rename to assets/data/img/flags/PG.png diff --git a/assets/img/flags/PH.png b/assets/data/img/flags/PH.png similarity index 100% rename from assets/img/flags/PH.png rename to assets/data/img/flags/PH.png diff --git a/assets/img/flags/PK.png b/assets/data/img/flags/PK.png similarity index 100% rename from assets/img/flags/PK.png rename to assets/data/img/flags/PK.png diff --git a/assets/img/flags/PL.png b/assets/data/img/flags/PL.png similarity index 100% rename from assets/img/flags/PL.png rename to assets/data/img/flags/PL.png diff --git a/assets/img/flags/PN.png b/assets/data/img/flags/PN.png similarity index 100% rename from assets/img/flags/PN.png rename to assets/data/img/flags/PN.png diff --git a/assets/img/flags/PR.png b/assets/data/img/flags/PR.png similarity index 100% rename from assets/img/flags/PR.png rename to assets/data/img/flags/PR.png diff --git a/assets/img/flags/PS.png b/assets/data/img/flags/PS.png similarity index 100% rename from assets/img/flags/PS.png rename to assets/data/img/flags/PS.png diff --git a/assets/img/flags/PT.png b/assets/data/img/flags/PT.png similarity index 100% rename from assets/img/flags/PT.png rename to assets/data/img/flags/PT.png diff --git a/assets/img/flags/PW.png b/assets/data/img/flags/PW.png similarity index 100% rename from assets/img/flags/PW.png rename to assets/data/img/flags/PW.png diff --git a/assets/img/flags/PY.png b/assets/data/img/flags/PY.png similarity index 100% rename from assets/img/flags/PY.png rename to assets/data/img/flags/PY.png diff --git a/assets/img/flags/QA.png b/assets/data/img/flags/QA.png similarity index 100% rename from assets/img/flags/QA.png rename to assets/data/img/flags/QA.png diff --git a/assets/img/flags/RE.png b/assets/data/img/flags/RE.png similarity index 100% rename from assets/img/flags/RE.png rename to assets/data/img/flags/RE.png diff --git a/assets/img/flags/RO.png b/assets/data/img/flags/RO.png similarity index 100% rename from assets/img/flags/RO.png rename to assets/data/img/flags/RO.png diff --git a/assets/img/flags/RS.png b/assets/data/img/flags/RS.png similarity index 100% rename from assets/img/flags/RS.png rename to assets/data/img/flags/RS.png diff --git a/assets/img/flags/RU.png b/assets/data/img/flags/RU.png similarity index 100% rename from assets/img/flags/RU.png rename to assets/data/img/flags/RU.png diff --git a/assets/img/flags/RW.png b/assets/data/img/flags/RW.png similarity index 100% rename from assets/img/flags/RW.png rename to assets/data/img/flags/RW.png diff --git a/assets/img/flags/SA.png b/assets/data/img/flags/SA.png similarity index 100% rename from assets/img/flags/SA.png rename to assets/data/img/flags/SA.png diff --git a/assets/img/flags/SB.png b/assets/data/img/flags/SB.png similarity index 100% rename from assets/img/flags/SB.png rename to assets/data/img/flags/SB.png diff --git a/assets/img/flags/SC.png b/assets/data/img/flags/SC.png similarity index 100% rename from assets/img/flags/SC.png rename to assets/data/img/flags/SC.png diff --git a/assets/img/flags/SD.png b/assets/data/img/flags/SD.png similarity index 100% rename from assets/img/flags/SD.png rename to assets/data/img/flags/SD.png diff --git a/assets/img/flags/SE.png b/assets/data/img/flags/SE.png similarity index 100% rename from assets/img/flags/SE.png rename to assets/data/img/flags/SE.png diff --git a/assets/img/flags/SG.png b/assets/data/img/flags/SG.png similarity index 100% rename from assets/img/flags/SG.png rename to assets/data/img/flags/SG.png diff --git a/assets/img/flags/SH.png b/assets/data/img/flags/SH.png similarity index 100% rename from assets/img/flags/SH.png rename to assets/data/img/flags/SH.png diff --git a/assets/img/flags/SI.png b/assets/data/img/flags/SI.png similarity index 100% rename from assets/img/flags/SI.png rename to assets/data/img/flags/SI.png diff --git a/assets/img/flags/SK.png b/assets/data/img/flags/SK.png similarity index 100% rename from assets/img/flags/SK.png rename to assets/data/img/flags/SK.png diff --git a/assets/img/flags/SL.png b/assets/data/img/flags/SL.png similarity index 100% rename from assets/img/flags/SL.png rename to assets/data/img/flags/SL.png diff --git a/assets/img/flags/SM.png b/assets/data/img/flags/SM.png similarity index 100% rename from assets/img/flags/SM.png rename to assets/data/img/flags/SM.png diff --git a/assets/img/flags/SN.png b/assets/data/img/flags/SN.png similarity index 100% rename from assets/img/flags/SN.png rename to assets/data/img/flags/SN.png diff --git a/assets/img/flags/SO.png b/assets/data/img/flags/SO.png similarity index 100% rename from assets/img/flags/SO.png rename to assets/data/img/flags/SO.png diff --git a/assets/img/flags/SR.png b/assets/data/img/flags/SR.png similarity index 100% rename from assets/img/flags/SR.png rename to assets/data/img/flags/SR.png diff --git a/assets/img/flags/SS.png b/assets/data/img/flags/SS.png similarity index 100% rename from assets/img/flags/SS.png rename to assets/data/img/flags/SS.png diff --git a/assets/img/flags/ST.png b/assets/data/img/flags/ST.png similarity index 100% rename from assets/img/flags/ST.png rename to assets/data/img/flags/ST.png diff --git a/assets/img/flags/SV.png b/assets/data/img/flags/SV.png similarity index 100% rename from assets/img/flags/SV.png rename to assets/data/img/flags/SV.png diff --git a/assets/img/flags/SX.png b/assets/data/img/flags/SX.png similarity index 100% rename from assets/img/flags/SX.png rename to assets/data/img/flags/SX.png diff --git a/assets/img/flags/SY.png b/assets/data/img/flags/SY.png similarity index 100% rename from assets/img/flags/SY.png rename to assets/data/img/flags/SY.png diff --git a/assets/img/flags/SZ.png b/assets/data/img/flags/SZ.png similarity index 100% rename from assets/img/flags/SZ.png rename to assets/data/img/flags/SZ.png diff --git a/assets/img/flags/TC.png b/assets/data/img/flags/TC.png similarity index 100% rename from assets/img/flags/TC.png rename to assets/data/img/flags/TC.png diff --git a/assets/img/flags/TD.png b/assets/data/img/flags/TD.png similarity index 100% rename from assets/img/flags/TD.png rename to assets/data/img/flags/TD.png diff --git a/assets/img/flags/TF.png b/assets/data/img/flags/TF.png similarity index 100% rename from assets/img/flags/TF.png rename to assets/data/img/flags/TF.png diff --git a/assets/img/flags/TG.png b/assets/data/img/flags/TG.png similarity index 100% rename from assets/img/flags/TG.png rename to assets/data/img/flags/TG.png diff --git a/assets/img/flags/TH.png b/assets/data/img/flags/TH.png similarity index 100% rename from assets/img/flags/TH.png rename to assets/data/img/flags/TH.png diff --git a/assets/img/flags/TJ.png b/assets/data/img/flags/TJ.png similarity index 100% rename from assets/img/flags/TJ.png rename to assets/data/img/flags/TJ.png diff --git a/assets/img/flags/TK.png b/assets/data/img/flags/TK.png similarity index 100% rename from assets/img/flags/TK.png rename to assets/data/img/flags/TK.png diff --git a/assets/img/flags/TL.png b/assets/data/img/flags/TL.png similarity index 100% rename from assets/img/flags/TL.png rename to assets/data/img/flags/TL.png diff --git a/assets/img/flags/TM.png b/assets/data/img/flags/TM.png similarity index 100% rename from assets/img/flags/TM.png rename to assets/data/img/flags/TM.png diff --git a/assets/img/flags/TN.png b/assets/data/img/flags/TN.png similarity index 100% rename from assets/img/flags/TN.png rename to assets/data/img/flags/TN.png diff --git a/assets/img/flags/TO.png b/assets/data/img/flags/TO.png similarity index 100% rename from assets/img/flags/TO.png rename to assets/data/img/flags/TO.png diff --git a/assets/img/flags/TR.png b/assets/data/img/flags/TR.png similarity index 100% rename from assets/img/flags/TR.png rename to assets/data/img/flags/TR.png diff --git a/assets/img/flags/TT.png b/assets/data/img/flags/TT.png similarity index 100% rename from assets/img/flags/TT.png rename to assets/data/img/flags/TT.png diff --git a/assets/img/flags/TV.png b/assets/data/img/flags/TV.png similarity index 100% rename from assets/img/flags/TV.png rename to assets/data/img/flags/TV.png diff --git a/assets/img/flags/TW.png b/assets/data/img/flags/TW.png similarity index 100% rename from assets/img/flags/TW.png rename to assets/data/img/flags/TW.png diff --git a/assets/img/flags/TZ.png b/assets/data/img/flags/TZ.png similarity index 100% rename from assets/img/flags/TZ.png rename to assets/data/img/flags/TZ.png diff --git a/assets/img/flags/UA.png b/assets/data/img/flags/UA.png similarity index 100% rename from assets/img/flags/UA.png rename to assets/data/img/flags/UA.png diff --git a/assets/img/flags/UG.png b/assets/data/img/flags/UG.png similarity index 100% rename from assets/img/flags/UG.png rename to assets/data/img/flags/UG.png diff --git a/assets/img/flags/US.png b/assets/data/img/flags/US.png similarity index 100% rename from assets/img/flags/US.png rename to assets/data/img/flags/US.png diff --git a/assets/img/flags/UY.png b/assets/data/img/flags/UY.png similarity index 100% rename from assets/img/flags/UY.png rename to assets/data/img/flags/UY.png diff --git a/assets/img/flags/UZ.png b/assets/data/img/flags/UZ.png similarity index 100% rename from assets/img/flags/UZ.png rename to assets/data/img/flags/UZ.png diff --git a/assets/img/flags/VA.png b/assets/data/img/flags/VA.png similarity index 100% rename from assets/img/flags/VA.png rename to assets/data/img/flags/VA.png diff --git a/assets/img/flags/VC.png b/assets/data/img/flags/VC.png similarity index 100% rename from assets/img/flags/VC.png rename to assets/data/img/flags/VC.png diff --git a/assets/img/flags/VE.png b/assets/data/img/flags/VE.png similarity index 100% rename from assets/img/flags/VE.png rename to assets/data/img/flags/VE.png diff --git a/assets/img/flags/VG.png b/assets/data/img/flags/VG.png similarity index 100% rename from assets/img/flags/VG.png rename to assets/data/img/flags/VG.png diff --git a/assets/img/flags/VI.png b/assets/data/img/flags/VI.png similarity index 100% rename from assets/img/flags/VI.png rename to assets/data/img/flags/VI.png diff --git a/assets/img/flags/VN.png b/assets/data/img/flags/VN.png similarity index 100% rename from assets/img/flags/VN.png rename to assets/data/img/flags/VN.png diff --git a/assets/img/flags/VU.png b/assets/data/img/flags/VU.png similarity index 100% rename from assets/img/flags/VU.png rename to assets/data/img/flags/VU.png diff --git a/assets/img/flags/WF.png b/assets/data/img/flags/WF.png similarity index 100% rename from assets/img/flags/WF.png rename to assets/data/img/flags/WF.png diff --git a/assets/img/flags/WS.png b/assets/data/img/flags/WS.png similarity index 100% rename from assets/img/flags/WS.png rename to assets/data/img/flags/WS.png diff --git a/assets/img/flags/YE.png b/assets/data/img/flags/YE.png similarity index 100% rename from assets/img/flags/YE.png rename to assets/data/img/flags/YE.png diff --git a/assets/img/flags/YT.png b/assets/data/img/flags/YT.png similarity index 100% rename from assets/img/flags/YT.png rename to assets/data/img/flags/YT.png diff --git a/assets/img/flags/ZA.png b/assets/data/img/flags/ZA.png similarity index 100% rename from assets/img/flags/ZA.png rename to assets/data/img/flags/ZA.png diff --git a/assets/img/flags/ZM.png b/assets/data/img/flags/ZM.png similarity index 100% rename from assets/img/flags/ZM.png rename to assets/data/img/flags/ZM.png diff --git a/assets/img/flags/ZW.png b/assets/data/img/flags/ZW.png similarity index 100% rename from assets/img/flags/ZW.png rename to assets/data/img/flags/ZW.png diff --git a/assets/img/flags/_abkhazia.png b/assets/data/img/flags/_abkhazia.png similarity index 100% rename from assets/img/flags/_abkhazia.png rename to assets/data/img/flags/_abkhazia.png diff --git a/assets/img/flags/_basque-country.png b/assets/data/img/flags/_basque-country.png similarity index 100% rename from assets/img/flags/_basque-country.png rename to assets/data/img/flags/_basque-country.png diff --git a/assets/img/flags/_british-antarctic-territory.png b/assets/data/img/flags/_british-antarctic-territory.png similarity index 100% rename from assets/img/flags/_british-antarctic-territory.png rename to assets/data/img/flags/_british-antarctic-territory.png diff --git a/assets/img/flags/_commonwealth.png b/assets/data/img/flags/_commonwealth.png similarity index 100% rename from assets/img/flags/_commonwealth.png rename to assets/data/img/flags/_commonwealth.png diff --git a/assets/img/flags/_england.png b/assets/data/img/flags/_england.png similarity index 100% rename from assets/img/flags/_england.png rename to assets/data/img/flags/_england.png diff --git a/assets/img/flags/_gosquared.png b/assets/data/img/flags/_gosquared.png similarity index 100% rename from assets/img/flags/_gosquared.png rename to assets/data/img/flags/_gosquared.png diff --git a/assets/img/flags/_kosovo.png b/assets/data/img/flags/_kosovo.png similarity index 100% rename from assets/img/flags/_kosovo.png rename to assets/data/img/flags/_kosovo.png diff --git a/assets/img/flags/_mars.png b/assets/data/img/flags/_mars.png similarity index 100% rename from assets/img/flags/_mars.png rename to assets/data/img/flags/_mars.png diff --git a/assets/img/flags/_nagorno-karabakh.png b/assets/data/img/flags/_nagorno-karabakh.png similarity index 100% rename from assets/img/flags/_nagorno-karabakh.png rename to assets/data/img/flags/_nagorno-karabakh.png diff --git a/assets/img/flags/_nato.png b/assets/data/img/flags/_nato.png similarity index 100% rename from assets/img/flags/_nato.png rename to assets/data/img/flags/_nato.png diff --git a/assets/img/flags/_northern-cyprus.png b/assets/data/img/flags/_northern-cyprus.png similarity index 100% rename from assets/img/flags/_northern-cyprus.png rename to assets/data/img/flags/_northern-cyprus.png diff --git a/assets/img/flags/_olympics.png b/assets/data/img/flags/_olympics.png similarity index 100% rename from assets/img/flags/_olympics.png rename to assets/data/img/flags/_olympics.png diff --git a/assets/img/flags/_red-cross.png b/assets/data/img/flags/_red-cross.png similarity index 100% rename from assets/img/flags/_red-cross.png rename to assets/data/img/flags/_red-cross.png diff --git a/assets/img/flags/_scotland.png b/assets/data/img/flags/_scotland.png similarity index 100% rename from assets/img/flags/_scotland.png rename to assets/data/img/flags/_scotland.png diff --git a/assets/img/flags/_somaliland.png b/assets/data/img/flags/_somaliland.png similarity index 100% rename from assets/img/flags/_somaliland.png rename to assets/data/img/flags/_somaliland.png diff --git a/assets/img/flags/_south-ossetia.png b/assets/data/img/flags/_south-ossetia.png similarity index 100% rename from assets/img/flags/_south-ossetia.png rename to assets/data/img/flags/_south-ossetia.png diff --git a/assets/img/flags/_united-nations.png b/assets/data/img/flags/_united-nations.png similarity index 100% rename from assets/img/flags/_united-nations.png rename to assets/data/img/flags/_united-nations.png diff --git a/assets/img/flags/_unknown.png b/assets/data/img/flags/_unknown.png similarity index 100% rename from assets/img/flags/_unknown.png rename to assets/data/img/flags/_unknown.png diff --git a/assets/img/flags/_wales.png b/assets/data/img/flags/_wales.png similarity index 100% rename from assets/img/flags/_wales.png rename to assets/data/img/flags/_wales.png diff --git a/assets/img/linux.svg b/assets/data/img/linux.svg similarity index 100% rename from assets/img/linux.svg rename to assets/data/img/linux.svg diff --git a/assets/img/mac.svg b/assets/data/img/mac.svg similarity index 100% rename from assets/img/mac.svg rename to assets/data/img/mac.svg diff --git a/assets/img/plants1-br.png b/assets/data/img/plants1-br.png similarity index 100% rename from assets/img/plants1-br.png rename to assets/data/img/plants1-br.png diff --git a/assets/img/plants1.png b/assets/data/img/plants1.png similarity index 100% rename from assets/img/plants1.png rename to assets/data/img/plants1.png diff --git a/assets/img/spn-feature-carousel/access-regional-content-easily.png b/assets/data/img/spn-feature-carousel/access-regional-content-easily.png similarity index 100% rename from assets/img/spn-feature-carousel/access-regional-content-easily.png rename to assets/data/img/spn-feature-carousel/access-regional-content-easily.png diff --git a/assets/img/spn-feature-carousel/built-from-the-ground-up.png b/assets/data/img/spn-feature-carousel/built-from-the-ground-up.png similarity index 100% rename from assets/img/spn-feature-carousel/built-from-the-ground-up.png rename to assets/data/img/spn-feature-carousel/built-from-the-ground-up.png diff --git a/assets/img/spn-feature-carousel/bye-bye-vpns.png b/assets/data/img/spn-feature-carousel/bye-bye-vpns.png similarity index 100% rename from assets/img/spn-feature-carousel/bye-bye-vpns.png rename to assets/data/img/spn-feature-carousel/bye-bye-vpns.png diff --git a/assets/img/spn-feature-carousel/easily-control-your-privacy.png b/assets/data/img/spn-feature-carousel/easily-control-your-privacy.png similarity index 100% rename from assets/img/spn-feature-carousel/easily-control-your-privacy.png rename to assets/data/img/spn-feature-carousel/easily-control-your-privacy.png diff --git a/assets/img/spn-feature-carousel/multiple-identities-for-each-app.png b/assets/data/img/spn-feature-carousel/multiple-identities-for-each-app.png similarity index 100% rename from assets/img/spn-feature-carousel/multiple-identities-for-each-app.png rename to assets/data/img/spn-feature-carousel/multiple-identities-for-each-app.png diff --git a/assets/img/spn-login.png b/assets/data/img/spn-login.png similarity index 100% rename from assets/img/spn-login.png rename to assets/data/img/spn-login.png diff --git a/assets/img/windows.svg b/assets/data/img/windows.svg similarity index 100% rename from assets/img/windows.svg rename to assets/data/img/windows.svg diff --git a/assets/world-50m.json b/assets/data/world-50m.json similarity index 100% rename from assets/world-50m.json rename to assets/data/world-50m.json diff --git a/assets/icons.go b/assets/icons.go new file mode 100644 index 00000000..7ffa780d --- /dev/null +++ b/assets/icons.go @@ -0,0 +1,8 @@ +package assets + +import ( + _ "embed" +) + +//go:embed data/icons/pm_light_512.png +var PNG []byte diff --git a/assets/icons_default.go b/assets/icons_default.go new file mode 100644 index 00000000..2c1b6eb1 --- /dev/null +++ b/assets/icons_default.go @@ -0,0 +1,102 @@ +//go:build !windows + +package assets + +import ( + "bytes" + _ "embed" + "fmt" + "image" + "image/png" + + "github.com/safing/portbase/log" + "golang.org/x/image/draw" +) + +// Colored Icon IDs. +const ( + GreenID = 0 + YellowID = 1 + RedID = 2 + BlueID = 3 +) + +// Icons. +var ( + //go:embed data/icons/pm_light_green_512.png + GreenPNG []byte + + //go:embed data/icons/pm_light_yellow_512.png + YellowPNG []byte + + //go:embed data/icons/pm_light_red_512.png + RedPNG []byte + + //go:embed data/icons/pm_light_blue_512.png + BluePNG []byte + + // ColoredIcons holds all the icons as .PNGs + ColoredIcons [4][]byte +) + +func init() { + setColoredIcons() +} + +func setColoredIcons() { + ColoredIcons = [4][]byte{ + GreenID: GreenPNG, + YellowID: YellowPNG, + RedID: RedPNG, + BlueID: BluePNG, + } +} + +// ScaleColoredIconsTo scales all colored icons to the given size. +// It must be called before any colored icons are used. +// It does nothing on Windows. +func ScaleColoredIconsTo(pixelSize int) { + // Scale colored icons only. + GreenPNG = quickScalePNG(GreenPNG, pixelSize) + YellowPNG = quickScalePNG(YellowPNG, pixelSize) + RedPNG = quickScalePNG(RedPNG, pixelSize) + BluePNG = quickScalePNG(BluePNG, pixelSize) + + // Repopulate colored icons. + setColoredIcons() +} + +func quickScalePNG(imgData []byte, pixelSize int) []byte { + scaledImage, err := scalePNGTo(imgData, pixelSize) + if err != nil { + log.Warningf("failed to scale image (using original): %s", err) + return imgData + } + return scaledImage +} + +func scalePNGTo(imgData []byte, pixelSize int) ([]byte, error) { + img, err := png.Decode(bytes.NewReader(imgData)) + if err != nil { + return nil, fmt.Errorf("failed to decode image: %w", err) + } + + // Return data unprocessed if image already has the correct size. + if img.Bounds().Dx() == pixelSize { + return imgData, nil + } + + // Scale image to given size. + rectangle := image.Rect(0, 0, pixelSize, pixelSize) + scaledImage := image.NewRGBA(rectangle) + draw.CatmullRom.Scale(scaledImage, rectangle, img, img.Bounds(), draw.Over, nil) + + // Encode scaled image. + scaledImgBuffer := new(bytes.Buffer) + err = png.Encode(scaledImgBuffer, scaledImage) + if err != nil { + return nil, fmt.Errorf("failed to encode image: %w", err) + } + + return scaledImgBuffer.Bytes(), nil +} diff --git a/assets/icons_windows.go b/assets/icons_windows.go new file mode 100644 index 00000000..83f5db2e --- /dev/null +++ b/assets/icons_windows.go @@ -0,0 +1,41 @@ +package assets + +import ( + _ "embed" +) + +// Colored Icon IDs. +const ( + GreenID = 0 + YellowID = 1 + RedID = 2 + BlueID = 3 +) + +// Icons. +var ( + //go:embed data/icons/pm_light_green_512.ico + GreenICO []byte + + //go:embed data/icons/pm_light_yellow_512.ico + YellowICO []byte + + //go:embed data/icons/pm_light_red_512.ico + RedICO []byte + + //go:embed data/icons/pm_light_blue_512.ico + BlueICO []byte + + // ColoredIcons holds all the icons as .ICOs + ColoredIcons = [4][]byte{ + GreenID: GreenICO, + YellowID: YellowICO, + RedID: RedICO, + BlueID: BlueICO, + } +) + +// ScaleColoredIconsTo scales all colored icons to the given size. +// It must be called before any colored icons are used. +// It does nothing on Windows. +func ScaleColoredIconsTo(pixelSize int) {} diff --git a/cmds/notifier/.gitignore b/cmds/notifier/.gitignore new file mode 100644 index 00000000..602ad23c --- /dev/null +++ b/cmds/notifier/.gitignore @@ -0,0 +1,34 @@ +# Compiled binaries +notifier +notifier.exe + +# Go vendor +vendor + +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out diff --git a/cmds/notifier/README.md b/cmds/notifier/README.md new file mode 100644 index 00000000..bdfcece8 --- /dev/null +++ b/cmds/notifier/README.md @@ -0,0 +1,5 @@ +### Development Dependencies + +sudo apt install libgtk-3-dev libayatana-appindicator3-dev libwebkitgtk-3.0-dev libgl1-mesa-dev libglu1-mesa-dev libnotify-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev + +sudo pacman -S libappindicator-gtk3 diff --git a/cmds/notifier/http_api.go b/cmds/notifier/http_api.go new file mode 100644 index 00000000..bb0eebf3 --- /dev/null +++ b/cmds/notifier/http_api.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/cookiejar" + "strings" + "time" + + "github.com/safing/portbase/log" +) + +const ( + apiBaseURL = "http://127.0.0.1:817/api/v1/" + apiShutdownEndpoint = "core/shutdown" +) + +var ( + httpApiClient *http.Client +) + +func init() { + // Make cookie jar. + jar, err := cookiejar.New(nil) + if err != nil { + log.Warningf("http-api: failed to create cookie jar: %s", err) + jar = nil + } + + // Create client. + httpApiClient = &http.Client{ + Jar: jar, + Timeout: 3 * time.Second, + } +} + +func httpApiAction(endpoint string) (response string, err error) { + // Make action request. + resp, err := httpApiClient.Post(apiBaseURL+endpoint, "", nil) + if err != nil { + return "", fmt.Errorf("request failed: %w", err) + } + + // Read the response body. + defer resp.Body.Close() + respData, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read data: %w", err) + } + response = strings.TrimSpace(string(respData)) + + // Check if the request was successful on the server. + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + return response, fmt.Errorf("server failed with %s: %s", resp.Status, response) + } + + return response, nil +} + +// TriggerShutdown triggers a shutdown via the APi. +func TriggerShutdown() error { + _, err := httpApiAction(apiShutdownEndpoint) + return err +} diff --git a/cmds/notifier/icons.go b/cmds/notifier/icons.go new file mode 100644 index 00000000..93b3db74 --- /dev/null +++ b/cmds/notifier/icons.go @@ -0,0 +1,25 @@ +package main + +import ( + "os" + "path/filepath" + "sync" + + icons "github.com/safing/portmaster/assets" +) + +var ( + appIconEnsureOnce sync.Once + appIconPath string +) + +func ensureAppIcon() (location string, err error) { + appIconEnsureOnce.Do(func() { + if appIconPath == "" { + appIconPath = filepath.Join(dataDir, "exec", "portmaster.png") + } + err = os.WriteFile(appIconPath, icons.PNG, 0o0644) + }) + + return appIconPath, err +} diff --git a/cmds/notifier/main.go b/cmds/notifier/main.go new file mode 100644 index 00000000..8bd95b3b --- /dev/null +++ b/cmds/notifier/main.go @@ -0,0 +1,286 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "os" + "os/signal" + "path/filepath" + "runtime" + "runtime/pprof" + "strings" + "sync" + "syscall" + "time" + + "github.com/tevino/abool" + + "github.com/safing/portbase/api/client" + "github.com/safing/portbase/dataroot" + "github.com/safing/portbase/info" + "github.com/safing/portbase/log" + "github.com/safing/portbase/modules" + "github.com/safing/portbase/updater" + "github.com/safing/portbase/utils" + "github.com/safing/portmaster/service/updates/helper" +) + +var ( + dataDir string + printStackOnExit bool + showVersion bool + + apiClient = client.NewClient("127.0.0.1:817") + connected = abool.New() + shuttingDown = abool.New() + restarting = abool.New() + + mainCtx, cancelMainCtx = context.WithCancel(context.Background()) + mainWg = &sync.WaitGroup{} + + dataRoot *utils.DirStructure + // Create registry. + registry = &updater.ResourceRegistry{ + Name: "updates", + UpdateURLs: []string{ + "https://updates.safing.io", + }, + DevMode: false, + Online: false, // disable download of resources (this is job for the core). + } +) + +func init() { + flag.StringVar(&dataDir, "data", "", "set data directory") + flag.BoolVar(&printStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down") + flag.BoolVar(&showVersion, "version", false, "show version and exit") + + runtime.GOMAXPROCS(2) +} + +func main() { + // parse flags + flag.Parse() + + // set meta info + info.Set("Portmaster Notifier", "0.3.6", "GPLv3", false) + + // check if meta info is ok + err := info.CheckVersion() + if err != nil { + fmt.Println("compile error: please compile using the provided build script") + os.Exit(1) + } + + // print help + if modules.HelpFlag { + flag.Usage() + os.Exit(0) + } + + if showVersion { + fmt.Println(info.FullVersion()) + os.Exit(0) + } + + // auto detect + if dataDir == "" { + dataDir = detectDataDir() + } + + // check data dir + if dataDir == "" { + fmt.Fprintln(os.Stderr, "please set the data directory using --data=/path/to/data/dir") + os.Exit(1) + } + + // switch to safe exec dir + err = os.Chdir(filepath.Join(dataDir, "exec")) + if err != nil { + fmt.Fprintf(os.Stderr, "warning: failed to switch to safe exec dir: %s\n", err) + } + + // start log writer + err = log.Start() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to start logging: %s\n", err) + os.Exit(1) + } + + // load registry + err = configureRegistry(true) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to load registry: %s\n", err) + os.Exit(1) + } + + // connect to API + go apiClient.StayConnected() + go apiStatusMonitor() + + // start subsystems + go tray() + go subsystemsClient() + go spnStatusClient() + go notifClient() + go startShutdownEventListener() + + // Shutdown + // catch interrupt for clean shutdown + signalCh := make(chan os.Signal, 1) + signal.Notify( + signalCh, + os.Interrupt, + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + ) + + // wait for shutdown + select { + case <-signalCh: + fmt.Println(" ") + log.Warning("program was interrupted, shutting down") + case <-mainCtx.Done(): + log.Warning("program is shutting down") + } + + if printStackOnExit { + fmt.Println("=== PRINTING STACK ===") + _ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 2) + fmt.Println("=== END STACK ===") + } + go func() { + time.Sleep(10 * time.Second) + fmt.Println("===== TAKING TOO LONG FOR SHUTDOWN - PRINTING STACK TRACES =====") + _ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 2) + os.Exit(1) + }() + + // clear all notifications + clearNotifications() + + // shutdown + cancelMainCtx() + mainWg.Wait() + + apiClient.Shutdown() + exitTray() + log.Shutdown() + + os.Exit(0) +} + +func apiStatusMonitor() { + for { + // Wait for connection. + <-apiClient.Online() + connected.Set() + triggerTrayUpdate() + + // Wait for lost connection. + <-apiClient.Offline() + connected.UnSet() + triggerTrayUpdate() + } +} + +func detectDataDir() string { + // get path of executable + binPath, err := os.Executable() + if err != nil { + return "" + } + // get directory + binDir := filepath.Dir(binPath) + // check if we in the updates directory + identifierDir := filepath.Join("updates", runtime.GOOS+"_"+runtime.GOARCH, "notifier") + // check if there is a match and return data dir + if strings.HasSuffix(binDir, identifierDir) { + return filepath.Clean(strings.TrimSuffix(binDir, identifierDir)) + } + return "" +} + +func configureRegistry(mustLoadIndex bool) error { + // If dataDir is not set, check the environment variable. + if dataDir == "" { + dataDir = os.Getenv("PORTMASTER_DATA") + } + + // If it's still empty, try to auto-detect it. + if dataDir == "" { + dataDir = detectInstallationDir() + } + + // Finally, if it's still empty, the user must provide it. + if dataDir == "" { + return errors.New("please set the data directory using --data=/path/to/data/dir") + } + + // Remove left over quotes. + dataDir = strings.Trim(dataDir, `\"`) + // Initialize data root. + err := dataroot.Initialize(dataDir, 0o0755) + if err != nil { + return fmt.Errorf("failed to initialize data root: %w", err) + } + dataRoot = dataroot.Root() + + // Initialize registry. + err = registry.Initialize(dataRoot.ChildDir("updates", 0o0755)) + if err != nil { + return err + } + + return updateRegistryIndex(mustLoadIndex) +} + +func detectInstallationDir() string { + exePath, err := filepath.Abs(os.Args[0]) + if err != nil { + return "" + } + + parent := filepath.Dir(exePath) // parent should be "...\updates\windows_amd64\notifier" + stableJSONFile := filepath.Join(parent, "..", "..", "stable.json") // "...\updates\stable.json" + stat, err := os.Stat(stableJSONFile) + if err != nil { + return "" + } + + if stat.IsDir() { + return "" + } + + return parent +} + +func updateRegistryIndex(mustLoadIndex bool) error { + // Set indexes based on the release channel. + warning := helper.SetIndexes(registry, "", false, false, false) + if warning != nil { + log.Warningf("%q", warning) + } + + // Load indexes from disk or network, if needed and desired. + err := registry.LoadIndexes(context.Background()) + if err != nil { + log.Warningf("error loading indexes %q", warning) + if mustLoadIndex { + return err + } + } + + // Load versions from disk to know which others we have and which are available. + err = registry.ScanStorage("") + if err != nil { + log.Warningf("error during storage scan: %q\n", err) + } + + registry.SelectVersions() + return nil +} diff --git a/cmds/notifier/notification.go b/cmds/notifier/notification.go new file mode 100644 index 00000000..fc37690b --- /dev/null +++ b/cmds/notifier/notification.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + + pbnotify "github.com/safing/portbase/notifications" +) + +// Notification represents a notification that is to be delivered to the user. +type Notification struct { + pbnotify.Notification + + // systemID holds the ID returned by the dbus interface on Linux or by WinToast library on Windows. + systemID NotificationID +} + +// IsSupported returns whether the action is supported on this system. +func IsSupportedAction(a pbnotify.Action) bool { + switch a.Type { + case pbnotify.ActionTypeNone: + return true + default: + return false + } +} + +// SelectAction sends an action back to the portmaster. +func (n *Notification) SelectAction(action string) { + new := &pbnotify.Notification{ + EventID: n.EventID, + SelectedActionID: action, + } + + // FIXME: check response + apiClient.Update(fmt.Sprintf("%s%s", dbNotifBasePath, new.EventID), new, nil) +} diff --git a/cmds/notifier/notify.go b/cmds/notifier/notify.go new file mode 100644 index 00000000..1b271b67 --- /dev/null +++ b/cmds/notifier/notify.go @@ -0,0 +1,103 @@ +package main + +import ( + "fmt" + "strings" + "sync" + "time" + + "github.com/safing/portbase/api/client" + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portbase/log" + + pbnotify "github.com/safing/portbase/notifications" +) + +const ( + dbNotifBasePath = "notifications:all/" +) + +var ( + notifications = make(map[string]*Notification) + notificationsLock sync.Mutex +) + +func notifClient() { + notifOp := apiClient.Qsub(fmt.Sprintf("query %s where ShowOnSystem is true", dbNotifBasePath), handleNotification) + notifOp.EnableResuscitation() + + // start the action listener and block + // until it's closed. + actionListener() +} + +func handleNotification(m *client.Message) { + notificationsLock.Lock() + defer notificationsLock.Unlock() + + log.Tracef("received %s msg: %s", m.Type, m.Key) + + switch m.Type { + case client.MsgError: + case client.MsgDone: + case client.MsgSuccess: + case client.MsgOk, client.MsgUpdate, client.MsgNew: + + n := &Notification{} + _, err := dsd.Load(m.RawValue, &n.Notification) + if err != nil { + log.Warningf("notify: failed to parse new notification: %s", err) + return + } + + // copy existing system values + existing, ok := notifications[n.EventID] + if ok { + existing.Lock() + n.systemID = existing.systemID + existing.Unlock() + } + + // save + notifications[n.EventID] = n + + // Handle notification. + switch { + case existing != nil: + // Cancel existing notification if not active, else ignore. + if n.State != pbnotify.Active { + existing.Cancel() + } + return + case n.State == pbnotify.Active: + // Show new notifications that are active. + n.Show() + default: + // Ignore new notifications that are not active. + } + + case client.MsgDelete: + + n, ok := notifications[strings.TrimPrefix(m.Key, dbNotifBasePath)] + if ok { + n.Cancel() + delete(notifications, n.EventID) + } + + case client.MsgWarning: + case client.MsgOffline: + } +} + +func clearNotifications() { + notificationsLock.Lock() + defer notificationsLock.Unlock() + + for _, n := range notifications { + n.Cancel() + } + + // Wait for goroutines that cancel notifications. + // TODO: Revamp to use a waitgroup. + time.Sleep(1 * time.Second) +} diff --git a/cmds/notifier/notify_linux.go b/cmds/notifier/notify_linux.go new file mode 100644 index 00000000..bcf650cf --- /dev/null +++ b/cmds/notifier/notify_linux.go @@ -0,0 +1,154 @@ +package main + +import ( + "context" + "sync" + + notify "github.com/dhaavi/go-notify" + "github.com/safing/portbase/log" +) + +type NotificationID uint32 + +var ( + capabilities notify.Capabilities + notifsByID sync.Map +) + +func init() { + var err error + capabilities, err = notify.GetCapabilities() + if err != nil { + log.Errorf("failed to get notification system capabilities: %s", err) + } +} + +func handleActions(ctx context.Context, actions chan notify.Signal) { + mainWg.Add(1) + defer mainWg.Done() + +listenForNotifications: + for { + select { + case <-ctx.Done(): + return + case sig := <-actions: + if sig.Name != "org.freedesktop.Notifications.ActionInvoked" { + // we don't care for anything else (dismissed, closed) + continue listenForNotifications + } + + // get notification by system ID + n, ok := notifsByID.LoadAndDelete(NotificationID(sig.ID)) + + if !ok { + continue listenForNotifications + } + + notification := n.(*Notification) + + log.Tracef("notify: received signal: %+v", sig) + if sig.ActionKey != "" { + // send action + if ok { + notification.Lock() + notification.SelectAction(sig.ActionKey) + notification.Unlock() + } + } else { + log.Tracef("notify: notification clicked: %+v", sig) + // Global action invoked, start the app + launchApp() + } + } + } + +} + +func actionListener() { + actions := make(chan notify.Signal, 100) + + go handleActions(mainCtx, actions) + + err := notify.SignalNotify(mainCtx, actions) + if err != nil && err != context.Canceled { + log.Errorf("notify: signal listener failed: %s", err) + } +} + +// Show shows the notification. +func (n *Notification) Show() { + sysN := notify.NewNotification("Portmaster", n.Message) + // see https://developer.gnome.org/notification-spec/ + + // The optional name of the application sending the notification. + // Can be blank. + sysN.AppName = "Portmaster" + + // The optional notification ID that this notification replaces. + sysN.ReplacesID = uint32(n.systemID) + + // The optional program icon of the calling application. + // sysN.AppIcon string + + // The summary text briefly describing the notification. + // Summary string (arg 1) + + // The optional detailed body text. + // Body string (arg 2) + + // The actions send a request message back to the notification client + // when invoked. + // sysN.Actions []string + if capabilities.Actions { + sysN.Actions = make([]string, 0, len(n.AvailableActions)*2) + for _, action := range n.AvailableActions { + if IsSupportedAction(*action) { + sysN.Actions = append(sysN.Actions, action.ID) + sysN.Actions = append(sysN.Actions, action.Text) + } + } + } + + // Set Portmaster icon. + iconLocation, err := ensureAppIcon() + if err != nil { + log.Warningf("notify: failed to write icon: %s", err) + } + sysN.AppIcon = iconLocation + + // TODO: Use hints to display icon of affected app. + // Hints are a way to provide extra data to a notification server. + // sysN.Hints = make(map[string]interface{}) + + // The timeout time in milliseconds since the display of the + // notification at which the notification should automatically close. + // sysN.Timeout int32 + + newID, err := sysN.Show() + if err != nil { + log.Warningf("notify: failed to show notification %s", n.EventID) + return + } + + notifsByID.Store(NotificationID(newID), n) + + n.Lock() + defer n.Unlock() + n.systemID = NotificationID(newID) +} + +// Cancel cancels the notification. +func (n *Notification) Cancel() { + n.Lock() + defer n.Unlock() + + // TODO: could a ID of 0 be valid? + if n.systemID != 0 { + err := notify.CloseNotification(uint32(n.systemID)) + if err != nil { + log.Warningf("notify: failed to close notification %s/%d", n.EventID, n.systemID) + } + notifsByID.Delete(n.systemID) + } +} diff --git a/cmds/notifier/notify_windows.go b/cmds/notifier/notify_windows.go new file mode 100644 index 00000000..abb56be0 --- /dev/null +++ b/cmds/notifier/notify_windows.go @@ -0,0 +1,184 @@ +package main + +import ( + "fmt" + "sync" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/cmds/notifier/wintoast" + "github.com/safing/portmaster/service/updates/helper" +) + +type NotificationID int64 + +const ( + appName = "Portmaster" + appUserModelID = "io.safing.portmaster.2" + originalShortcutPath = "C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Portmaster\\Portmaster.lnk" +) + +const ( + SoundDefault = 0 + SoundSilent = 1 + SoundLoop = 2 +) + +const ( + SoundPathDefault = 0 + // see notification_glue.h if you need more types +) + +var ( + initOnce sync.Once + lib *wintoast.WinToast + notificationsByIDs sync.Map +) + +func getLib() *wintoast.WinToast { + initOnce.Do(func() { + dllPath, err := getDllPath() + if err != nil { + log.Errorf("notify: failed to get dll path: %s", err) + return + } + // Load dll and all the functions + newLib, err := wintoast.New(dllPath) + if err != nil { + log.Errorf("notify: failed to load library: %s", err) + return + } + + // Initialize. This will create or update application shortcut. C:\Users\\AppData\Roaming\Microsoft\Windows\Start Menu\Programs + // and it will be of the originalShortcutPath with no CLSID and different AUMI + err = newLib.Initialize(appName, appUserModelID, originalShortcutPath) + if err != nil { + log.Errorf("notify: failed to load library: %s", err) + return + } + + // library was initialized successfully + lib = newLib + + // Set callbacks + + err = lib.SetCallbacks(notificationActivatedCallback, notificationDismissedCallback, notificationDismissedCallback) + if err != nil { + log.Warningf("notify: failed to set callbacks: %s", err) + return + } + }) + + return lib +} + +// Show shows the notification. +func (n *Notification) Show() { + // Lock notification + n.Lock() + defer n.Unlock() + + // Create new notification object + builder, err := getLib().NewNotification(n.Title, n.Message) + if err != nil { + log.Errorf("notify: failed to create notification: %s", err) + return + } + // Make sure memory is freed when done + defer builder.Delete() + + // if needed set notification icon + // _ = builder.SetImage(iconLocation) + + // Leaving the default value for the sound + // _ = builder.SetSound(SoundDefault, SoundPathDefault) + + // Set all the required actions. + for _, action := range n.AvailableActions { + err = builder.AddButton(action.Text) + if err != nil { + log.Warningf("notify: failed to add button: %s", err) + } + } + + // Show notification. + id, err := builder.Show() + if err != nil { + log.Errorf("notify: failed to show notification: %s", err) + return + } + n.systemID = NotificationID(id) + + // Link system id to the notification object + notificationsByIDs.Store(NotificationID(id), n) + + log.Debugf("notify: showing notification %q: %d", n.Title, n.systemID) +} + +// Cancel cancels the notification. +func (n *Notification) Cancel() { + // Lock notification + n.Lock() + defer n.Unlock() + + // No need to check for errors. If it fails it is probably already dismissed + _ = getLib().HideNotification(int64(n.systemID)) + + notificationsByIDs.Delete(n.systemID) + log.Debugf("notify: notification canceled %q: %d", n.Title, n.systemID) +} + +func notificationActivatedCallback(id int64, actionIndex int32) { + if actionIndex == -1 { + // The user clicked on the notification (not a button), open the portmaster and delete + launchApp() + notificationsByIDs.Delete(NotificationID(id)) + log.Debugf("notify: notification clicked %d", id) + return + } + + // The user click one of the buttons + + // Get notified object + n, ok := notificationsByIDs.LoadAndDelete(NotificationID(id)) + if !ok { + return + } + + notification := n.(*Notification) + + notification.Lock() + defer notification.Unlock() + + // Set selected action + actionID := notification.AvailableActions[actionIndex].ID + notification.SelectAction(actionID) + + log.Debugf("notify: notification button cliecked %d button id: %d", id, actionIndex) +} + +func notificationDismissedCallback(id int64, reason int32) { + // Failure or user dismissed the notification + if reason == 0 { + notificationsByIDs.Delete(NotificationID(id)) + log.Debugf("notify: notification dissmissed %d", id) + } +} + +func getDllPath() (string, error) { + if dataDir == "" { + return "", fmt.Errorf("dataDir is empty") + } + + // Aks the registry for the dll path + identifier := helper.PlatformIdentifier("notifier/portmaster-wintoast.dll") + file, err := registry.GetFile(identifier) + if err != nil { + return "", err + } + return file.Path(), nil +} + +func actionListener() { + // initialize the library + _ = getLib() +} diff --git a/cmds/notifier/shutdown.go b/cmds/notifier/shutdown.go new file mode 100644 index 00000000..f943938d --- /dev/null +++ b/cmds/notifier/shutdown.go @@ -0,0 +1,50 @@ +package main + +import ( + "github.com/safing/portbase/api/client" + "github.com/safing/portbase/log" +) + +func startShutdownEventListener() { + shutdownNotifOp := apiClient.Sub("query runtime:modules/core/event/shutdown", handleShutdownEvent) + shutdownNotifOp.EnableResuscitation() + + restartNotifOp := apiClient.Sub("query runtime:modules/core/event/restart", handleRestartEvent) + restartNotifOp.EnableResuscitation() +} + +func handleShutdownEvent(m *client.Message) { + switch m.Type { + case client.MsgOk, client.MsgUpdate, client.MsgNew: + shuttingDown.Set() + triggerTrayUpdate() + + log.Warningf("shutdown: received shutdown event, shutting down now") + + // wait for the API client connection to die + <-apiClient.Offline() + shuttingDown.UnSet() + + cancelMainCtx() + + case client.MsgWarning, client.MsgError: + log.Errorf("shutdown: event subscription error: %s", string(m.RawValue)) + } +} + +func handleRestartEvent(m *client.Message) { + switch m.Type { + case client.MsgOk, client.MsgUpdate, client.MsgNew: + restarting.Set() + triggerTrayUpdate() + + log.Warningf("restart: received restart event") + + // wait for the API client connection to die + <-apiClient.Offline() + restarting.UnSet() + triggerTrayUpdate() + case client.MsgWarning, client.MsgError: + log.Errorf("shutdown: event subscription error: %s", string(m.RawValue)) + } +} diff --git a/cmds/notifier/snoretoast-guid.patch b/cmds/notifier/snoretoast-guid.patch new file mode 100644 index 00000000..1a050e5f --- /dev/null +++ b/cmds/notifier/snoretoast-guid.patch @@ -0,0 +1,15 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 498226a..446ba5e 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -2,7 +2,9 @@ cmake_minimum_required(VERSION 3.4) + + project(snoretoast VERSION 0.6.0) + # Always change the guid when the version is changed SNORETOAST_CALLBACK_GUID +-set(SNORETOAST_CALLBACK_GUID eb1fdd5b-8f70-4b5a-b230-998a2dc19303) ++#We keep it fixed! ++set(SNORETOAST_CALLBACK_GUID 7F00FB48-65D5-4BA8-A35B-F194DA7E1A51) ++ + + set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/) + diff --git a/cmds/notifier/spn.go b/cmds/notifier/spn.go new file mode 100644 index 00000000..1da5639d --- /dev/null +++ b/cmds/notifier/spn.go @@ -0,0 +1,103 @@ +package main + +import ( + "sync" + "time" + + "github.com/safing/portbase/api/client" + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portbase/log" + "github.com/tevino/abool" +) + +const ( + spnModuleKey = "config:spn/enable" + spnStatusKey = "runtime:spn/status" +) + +var ( + spnEnabled = abool.New() + + spnStatusCache *SPNStatus + spnStatusCacheLock sync.Mutex +) + +// SPNStatus holds SPN status information. +type SPNStatus struct { + Status string + HomeHubID string + HomeHubName string + ConnectedIP string + ConnectedTransport string + ConnectedSince *time.Time +} + +// GetSPNStatus returns the SPN status. +func GetSPNStatus() *SPNStatus { + spnStatusCacheLock.Lock() + defer spnStatusCacheLock.Unlock() + + return spnStatusCache +} + +func updateSPNStatus(s *SPNStatus) { + spnStatusCacheLock.Lock() + defer spnStatusCacheLock.Unlock() + + spnStatusCache = s +} + +func spnStatusClient() { + moduleQueryOp := apiClient.Qsub("query "+spnModuleKey, handleSPNModuleUpdate) + moduleQueryOp.EnableResuscitation() + + statusQueryOp := apiClient.Qsub("query "+spnStatusKey, handleSPNStatusUpdate) + statusQueryOp.EnableResuscitation() +} + +func handleSPNModuleUpdate(m *client.Message) { + switch m.Type { + case client.MsgOk, client.MsgUpdate, client.MsgNew: + var cfg struct { + Value bool `json:"Value"` + } + _, err := dsd.Load(m.RawValue, &cfg) + if err != nil { + log.Warningf("config: failed to parse config: %s", err) + return + } + log.Infof("config: received update to SPN module: enabled=%v", cfg.Value) + + spnEnabled.SetTo(cfg.Value) + triggerTrayUpdate() + + default: + } +} + +func handleSPNStatusUpdate(m *client.Message) { + switch m.Type { + case client.MsgOk, client.MsgUpdate, client.MsgNew: + newStatus := &SPNStatus{} + _, err := dsd.Load(m.RawValue, newStatus) + if err != nil { + log.Warningf("config: failed to parse config: %s", err) + return + } + log.Infof("config: received update to SPN status: %+v", newStatus) + + updateSPNStatus(newStatus) + triggerTrayUpdate() + + default: + } +} + +func ToggleSPN() { + var cfg struct { + Value bool `json:"Value"` + } + cfg.Value = !spnEnabled.IsSet() + + apiClient.Update(spnModuleKey, &cfg, nil) +} diff --git a/cmds/notifier/subsystems.go b/cmds/notifier/subsystems.go new file mode 100644 index 00000000..f810538c --- /dev/null +++ b/cmds/notifier/subsystems.go @@ -0,0 +1,122 @@ +package main + +import ( + "fmt" + "sync" + + "github.com/safing/portbase/api/client" + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portbase/log" +) + +const ( + subsystemsKeySpace = "runtime:subsystems/" + + // Module Failure Status Values + // FailureNone = 0 // unused + // FailureHint = 1 // unused + FailureWarning = 2 + FailureError = 3 +) + +var ( + subsystems = make(map[string]*Subsystem) + subsystemsLock sync.Mutex +) + +// Subsystem describes a subset of modules that represent a part of a +// service or program to the user. Subsystems can be (de-)activated causing +// all related modules to be brought down or up. +type Subsystem struct { //nolint:maligned // not worth the effort + // ID is a unique identifier for the subsystem. + ID string + + // Name holds a human readable name of the subsystem. + Name string + + // Description may holds an optional description of + // the subsystem's purpose. + Description string + + // Modules contains all modules that are related to the subsystem. + // Note that this slice also contains a reference to the subsystem + // module itself. + Modules []*ModuleStatus + + // FailureStatus is the worst failure status that is currently + // set in one of the subsystem's dependencies. + FailureStatus uint8 +} + +// ModuleStatus describes the status of a module. +type ModuleStatus struct { + Name string + Enabled bool + Status uint8 + FailureStatus uint8 + FailureID string + FailureMsg string +} + +// GetFailure returns the worst of all subsystem failures. +func GetFailure() (failureStatus uint8, failureMsg string) { + subsystemsLock.Lock() + defer subsystemsLock.Unlock() + + for _, subsystem := range subsystems { + for _, module := range subsystem.Modules { + if failureStatus < module.FailureStatus { + failureStatus = module.FailureStatus + failureMsg = module.FailureMsg + } + } + } + + return +} + +func updateSubsystem(s *Subsystem) { + subsystemsLock.Lock() + defer subsystemsLock.Unlock() + + subsystems[s.ID] = s +} + +func clearSubsystems() { + subsystemsLock.Lock() + defer subsystemsLock.Unlock() + + for key := range subsystems { + delete(subsystems, key) + } +} + +func subsystemsClient() { + subsystemsOp := apiClient.Qsub(fmt.Sprintf("query %s", subsystemsKeySpace), handleSubsystem) + subsystemsOp.EnableResuscitation() +} + +func handleSubsystem(m *client.Message) { + switch m.Type { + case client.MsgError: + case client.MsgDone: + case client.MsgSuccess: + case client.MsgOk, client.MsgUpdate, client.MsgNew: + + newSubsystem := &Subsystem{} + _, err := dsd.Load(m.RawValue, newSubsystem) + if err != nil { + log.Warningf("subsystems: failed to parse new subsystem: %s", err) + return + } + updateSubsystem(newSubsystem) + triggerTrayUpdate() + + case client.MsgDelete: + case client.MsgWarning: + case client.MsgOffline: + + clearSubsystems() + + } +} diff --git a/cmds/notifier/tray.go b/cmds/notifier/tray.go new file mode 100644 index 00000000..5766611f --- /dev/null +++ b/cmds/notifier/tray.go @@ -0,0 +1,218 @@ +package main + +import ( + "flag" + "os/exec" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + + "fyne.io/systray" + + "github.com/safing/portbase/log" + icons "github.com/safing/portmaster/assets" +) + +const ( + shortenStatusMsgTo = 40 +) + +var ( + trayLock sync.Mutex + + scaleColoredIconsTo int + + activeIconID int = -1 + activeStatusMsg = "" + activeSPNStatus = "" + activeSPNSwitch = "" + + menuItemStatusMsg *systray.MenuItem + menuItemSPNStatus *systray.MenuItem + menuItemSPNSwitch *systray.MenuItem +) + +func init() { + flag.IntVar(&scaleColoredIconsTo, "scale-icons", 32, "scale colored icons to given size in pixels") + + // lock until ready + trayLock.Lock() +} + +func tray() { + if scaleColoredIconsTo > 0 { + icons.ScaleColoredIconsTo(scaleColoredIconsTo) + } + + systray.Run(onReady, onExit) +} + +func exitTray() { + systray.Quit() +} + +func onReady() { + // unlock when ready + defer trayLock.Unlock() + + // icon + systray.SetIcon(icons.ColoredIcons[icons.RedID]) + if runtime.GOOS == "windows" { + // systray.SetTitle("Portmaster Notifier") // Don't set title, as it may be displayed in full in the menu/tray bar. (Ubuntu) + systray.SetTooltip("Portmaster Notifier") + } + + // menu: open app + if dataDir != "" { + menuItemOpenApp := systray.AddMenuItem("Open App", "") + go clickListener(menuItemOpenApp, launchApp) + systray.AddSeparator() + } + + // menu: status + + menuItemStatusMsg = systray.AddMenuItem("Loading...", "") + menuItemStatusMsg.Disable() + systray.AddSeparator() + + // menu: SPN + + menuItemSPNStatus = systray.AddMenuItem("Loading...", "") + menuItemSPNStatus.Disable() + menuItemSPNSwitch = systray.AddMenuItem("Loading...", "") + go clickListener(menuItemSPNSwitch, func() { + ToggleSPN() + }) + systray.AddSeparator() + + // menu: quit + systray.AddSeparator() + closeTray := systray.AddMenuItem("Close Tray Notifier", "") + go clickListener(closeTray, func() { + cancelMainCtx() + }) + shutdownPortmaster := systray.AddMenuItem("Shut Down Portmaster", "") + go clickListener(shutdownPortmaster, func() { + _ = TriggerShutdown() + time.Sleep(1 * time.Second) + cancelMainCtx() + }) +} + +func onExit() { + +} + +func triggerTrayUpdate() { + // TODO: Deduplicate triggers. + go updateTray() +} + +// updateTray update the state of the tray depending on the currently available information. +func updateTray() { + // Get current information. + spnStatus := GetSPNStatus() + failureID, failureMsg := GetFailure() + + trayLock.Lock() + defer trayLock.Unlock() + + // Select icon and status message to show. + newIconID := icons.GreenID + newStatusMsg := "Secure" + switch { + case shuttingDown.IsSet(): + newIconID = icons.RedID + newStatusMsg = "Shutting Down Portmaster" + + case restarting.IsSet(): + newIconID = icons.YellowID + newStatusMsg = "Restarting Portmaster" + + case !connected.IsSet(): + newIconID = icons.RedID + newStatusMsg = "Waiting for Portmaster Core Service" + + case failureID == FailureError: + newIconID = icons.RedID + newStatusMsg = failureMsg + + case failureID == FailureWarning: + newIconID = icons.YellowID + newStatusMsg = failureMsg + + case spnEnabled.IsSet(): + newIconID = icons.BlueID + } + + // Set icon if changed. + if newIconID != activeIconID { + activeIconID = newIconID + systray.SetIcon(icons.ColoredIcons[activeIconID]) + } + + // Set message if changed. + if newStatusMsg != activeStatusMsg { + activeStatusMsg = newStatusMsg + + // Shorten message if too long. + shortenedMsg := activeStatusMsg + if len(shortenedMsg) > shortenStatusMsgTo && strings.Contains(shortenedMsg, ". ") { + shortenedMsg = strings.SplitN(shortenedMsg, ". ", 2)[0] + } + if len(shortenedMsg) > shortenStatusMsgTo { + shortenedMsg = shortenedMsg[:shortenStatusMsgTo] + "..." + } + + menuItemStatusMsg.SetTitle("Status: " + shortenedMsg) + } + + // Set SPN status if changed. + if spnStatus != nil && activeSPNStatus != spnStatus.Status { + activeSPNStatus = spnStatus.Status + menuItemSPNStatus.SetTitle("SPN: " + strings.Title(activeSPNStatus)) + } + + // Set SPN switch if changed. + newSPNSwitch := "Enable SPN" + if spnEnabled.IsSet() { + newSPNSwitch = "Disable SPN" + } + if activeSPNSwitch != newSPNSwitch { + activeSPNSwitch = newSPNSwitch + menuItemSPNSwitch.SetTitle(activeSPNSwitch) + } +} + +func clickListener(item *systray.MenuItem, fn func()) { + for range item.ClickedCh { + fn() + } +} + +func launchApp() { + // build path to app + pmStartPath := filepath.Join(dataDir, "portmaster-start") + if runtime.GOOS == "windows" { + pmStartPath += ".exe" + } + + // start app + cmd := exec.Command(pmStartPath, "app", "--data", dataDir) + err := cmd.Start() + if err != nil { + log.Warningf("failed to start app: %s", err) + return + } + + // Use cmd.Wait() instead of cmd.Process.Release() to properly release its resources. + // See https://github.com/golang/go/issues/36534 + go func() { + err := cmd.Wait() + if err != nil { + log.Warningf("failed to wait/release app process: %s", err) + } + }() +} diff --git a/cmds/notifier/wintoast/notification_builder.go b/cmds/notifier/wintoast/notification_builder.go new file mode 100644 index 00000000..89eca798 --- /dev/null +++ b/cmds/notifier/wintoast/notification_builder.go @@ -0,0 +1,90 @@ +//go:build windows + +package wintoast + +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +type NotificationBuilder struct { + templatePointer uintptr + lib *WinToast +} + +func newNotification(lib *WinToast, title string, message string) (*NotificationBuilder, error) { + lib.Lock() + defer lib.Unlock() + + titleUTF, _ := windows.UTF16PtrFromString(title) + messageUTF, _ := windows.UTF16PtrFromString(message) + titleP := unsafe.Pointer(titleUTF) + messageP := unsafe.Pointer(messageUTF) + + ptr, _, err := lib.createNotification.Call(uintptr(titleP), uintptr(messageP)) + if ptr == 0 { + return nil, err + } + + return &NotificationBuilder{ptr, lib}, nil +} + +func (n *NotificationBuilder) Delete() { + if n == nil { + return + } + + n.lib.Lock() + defer n.lib.Unlock() + + _, _, _ = n.lib.deleteNotification.Call(n.templatePointer) +} + +func (n *NotificationBuilder) AddButton(text string) error { + n.lib.Lock() + defer n.lib.Unlock() + textUTF, _ := windows.UTF16PtrFromString(text) + textP := unsafe.Pointer(textUTF) + + rc, _, err := n.lib.addButton.Call(n.templatePointer, uintptr(textP)) + if rc != 1 { + return err + } + return nil +} + +func (n *NotificationBuilder) SetImage(iconPath string) error { + n.lib.Lock() + defer n.lib.Unlock() + pathUTF, _ := windows.UTF16PtrFromString(iconPath) + pathP := unsafe.Pointer(pathUTF) + + rc, _, err := n.lib.setImage.Call(n.templatePointer, uintptr(pathP)) + if rc != 1 { + return err + } + return nil +} + +func (n *NotificationBuilder) SetSound(option int, path int) error { + n.lib.Lock() + defer n.lib.Unlock() + + rc, _, err := n.lib.setSound.Call(n.templatePointer, uintptr(option), uintptr(path)) + if rc != 1 { + return err + } + return nil +} + +func (n *NotificationBuilder) Show() (int64, error) { + n.lib.Lock() + defer n.lib.Unlock() + + id, _, err := n.lib.showNotification.Call(n.templatePointer) + if int64(id) == -1 { + return -1, err + } + return int64(id), nil +} diff --git a/cmds/notifier/wintoast/wintoast.go b/cmds/notifier/wintoast/wintoast.go new file mode 100644 index 00000000..5d9a3380 --- /dev/null +++ b/cmds/notifier/wintoast/wintoast.go @@ -0,0 +1,217 @@ +//go:build windows + +package wintoast + +import ( + "fmt" + "sync" + "unsafe" + + "github.com/tevino/abool" + + "golang.org/x/sys/windows" +) + +// WinNotify holds the DLL handle. +type WinToast struct { + sync.RWMutex + + dll *windows.DLL + + initialized *abool.AtomicBool + + initialize *windows.Proc + isInitialized *windows.Proc + createNotification *windows.Proc + deleteNotification *windows.Proc + addButton *windows.Proc + setImage *windows.Proc + setSound *windows.Proc + showNotification *windows.Proc + hideNotification *windows.Proc + setActivatedCallback *windows.Proc + setDismissedCallback *windows.Proc + setFailedCallback *windows.Proc +} + +func New(dllPath string) (*WinToast, error) { + if dllPath == "" { + return nil, fmt.Errorf("winnotifiy: path to dll not specified") + } + + libraryObject := &WinToast{} + libraryObject.initialized = abool.New() + + // load dll + var err error + libraryObject.dll, err = windows.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("winnotifiy: failed to load notifier dll %w", err) + } + + // load functions + libraryObject.initialize, err = libraryObject.dll.FindProc("PortmasterToastInitialize") + if err != nil { + return nil, fmt.Errorf("winnotifiy: PortmasterToastInitialize not found %w", err) + } + + libraryObject.isInitialized, err = libraryObject.dll.FindProc("PortmasterToastIsInitialized") + if err != nil { + return nil, fmt.Errorf("winnotifiy: PortmasterToastIsInitialized not found %w", err) + } + + libraryObject.createNotification, err = libraryObject.dll.FindProc("PortmasterToastCreateNotification") + if err != nil { + return nil, fmt.Errorf("winnotifiy: PortmasterToastCreateNotification not found %w", err) + } + + libraryObject.deleteNotification, err = libraryObject.dll.FindProc("PortmasterToastDeleteNotification") + if err != nil { + return nil, fmt.Errorf("winnotifiy: PortmasterToastDeleteNotification not found %w", err) + } + + libraryObject.addButton, err = libraryObject.dll.FindProc("PortmasterToastAddButton") + if err != nil { + return nil, fmt.Errorf("winnotifiy: PortmasterToastAddButton not found %w", err) + } + + libraryObject.setImage, err = libraryObject.dll.FindProc("PortmasterToastSetImage") + if err != nil { + return nil, fmt.Errorf("winnotifiy: PortmasterToastSetImage not found %w", err) + } + + libraryObject.setSound, err = libraryObject.dll.FindProc("PortmasterToastSetSound") + if err != nil { + return nil, fmt.Errorf("winnotifiy: PortmasterToastSetSound not found %w", err) + } + + libraryObject.showNotification, err = libraryObject.dll.FindProc("PortmasterToastShow") + if err != nil { + return nil, fmt.Errorf("winnotifiy: PortmasterToastShow not found %w", err) + } + + libraryObject.setActivatedCallback, err = libraryObject.dll.FindProc("PortmasterToastActivatedCallback") + if err != nil { + return nil, fmt.Errorf("winnotifiy: PortmasterActivatedCallback not found %w", err) + } + + libraryObject.setDismissedCallback, err = libraryObject.dll.FindProc("PortmasterToastDismissedCallback") + if err != nil { + return nil, fmt.Errorf("winnotifiy: PortmasterToastDismissedCallback not found %w", err) + } + + libraryObject.setFailedCallback, err = libraryObject.dll.FindProc("PortmasterToastFailedCallback") + if err != nil { + return nil, fmt.Errorf("winnotifiy: PortmasterToastFailedCallback not found %w", err) + } + + libraryObject.hideNotification, err = libraryObject.dll.FindProc("PortmasterToastHide") + if err != nil { + return nil, fmt.Errorf("winnotifiy: PortmasterToastHide not found %w", err) + } + + return libraryObject, nil +} + +func (lib *WinToast) Initialize(appName, aumi, originalShortcutPath string) error { + if lib == nil { + return fmt.Errorf("wintoast: lib object was nil") + } + + lib.Lock() + defer lib.Unlock() + + // Initialize all necessary string for the notification meta data + appNameUTF, _ := windows.UTF16PtrFromString(appName) + aumiUTF, _ := windows.UTF16PtrFromString(aumi) + linkUTF, _ := windows.UTF16PtrFromString(originalShortcutPath) + + // They are needed as unsafe pointers + appNameP := unsafe.Pointer(appNameUTF) + aumiP := unsafe.Pointer(aumiUTF) + linkP := unsafe.Pointer(linkUTF) + + // Initialize notifications + rc, _, err := lib.initialize.Call(uintptr(appNameP), uintptr(aumiP), uintptr(linkP)) + if rc != 0 { + return fmt.Errorf("wintoast: failed to initialize library rc = %d, %w", rc, err) + } + + // Check if if the initialization was successfully + rc, _, _ = lib.isInitialized.Call() + if rc == 1 { + lib.initialized.Set() + } else { + return fmt.Errorf("wintoast: initialized flag was not set: rc = %d", rc) + } + + return nil +} + +func (lib *WinToast) SetCallbacks(activated func(id int64, actionIndex int32), dismissed func(id int64, reason int32), failed func(id int64, reason int32)) error { + if lib == nil { + return fmt.Errorf("wintoast: lib object was nil") + } + + if lib.initialized.IsNotSet() { + return fmt.Errorf("winnotifiy: library not initialized") + } + + // Initialize notification activated callback + callback := windows.NewCallback(func(id int64, actionIndex int32) uint64 { + activated(id, actionIndex) + return 0 + }) + rc, _, err := lib.setActivatedCallback.Call(callback) + if rc != 1 { + return fmt.Errorf("winnotifiy: failed to initialize activated callback %w", err) + } + + // Initialize notification dismissed callback + callback = windows.NewCallback(func(id int64, actionIndex int32) uint64 { + dismissed(id, actionIndex) + return 0 + }) + rc, _, err = lib.setDismissedCallback.Call(callback) + if rc != 1 { + return fmt.Errorf("winnotifiy: failed to initialize dismissed callback %w", err) + } + + // Initialize notification failed callback + callback = windows.NewCallback(func(id int64, actionIndex int32) uint64 { + failed(id, actionIndex) + return 0 + }) + rc, _, err = lib.setFailedCallback.Call(callback) + if rc != 1 { + return fmt.Errorf("winnotifiy: failed to initialize failed callback %s", err) + } + + return nil +} + +// NewNotification starts a creation of new notification. NotificationBuilder.Delete should allays be called when done using the object or there will be memory leeks +func (lib *WinToast) NewNotification(title string, content string) (*NotificationBuilder, error) { + if lib == nil { + return nil, fmt.Errorf("wintoast: lib object was nil") + } + return newNotification(lib, title, content) +} + +// HideNotification hides notification +func (lib *WinToast) HideNotification(id int64) error { + if lib == nil { + return fmt.Errorf("wintoast: lib object was nil") + } + + lib.Lock() + defer lib.Unlock() + + rc, _, _ := lib.hideNotification.Call(uintptr(id)) + + if rc != 1 { + return fmt.Errorf("wintoast: failed to hide notification %d", id) + } + + return nil +} diff --git a/desktop/angular/assets b/desktop/angular/assets index 41aef43f..21dab851 120000 --- a/desktop/angular/assets +++ b/desktop/angular/assets @@ -1 +1 @@ -../../assets \ No newline at end of file +../../assets/data \ No newline at end of file diff --git a/go.mod b/go.mod index 6a1fc4e3..f1b496f5 100644 --- a/go.mod +++ b/go.mod @@ -8,11 +8,13 @@ toolchain go1.21.2 replace github.com/tc-hib/winres => github.com/dhaavi/winres v0.2.2 require ( + fyne.io/systray v1.10.0 github.com/Xuanwo/go-locale v1.1.0 github.com/agext/levenshtein v1.2.3 github.com/awalterschulze/gographviz v2.0.3+incompatible github.com/cilium/ebpf v0.12.3 github.com/coreos/go-iptables v0.7.0 + github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435 github.com/florianl/go-conntrack v0.4.0 github.com/florianl/go-nfqueue v1.3.1 github.com/fogleman/gg v1.3.0 @@ -43,6 +45,7 @@ require ( github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26 github.com/vincent-petithory/dataurl v1.0.0 golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e + golang.org/x/image v0.15.0 golang.org/x/net v0.20.0 golang.org/x/sync v0.6.0 golang.org/x/sys v0.16.0 @@ -65,6 +68,7 @@ require ( github.com/fxamacker/cbor v1.5.1 // indirect github.com/fxamacker/cbor/v2 v2.5.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus v4.1.0+incompatible // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f // indirect @@ -106,7 +110,6 @@ require ( github.com/zeebo/blake3 v0.2.3 // indirect go.etcd.io/bbolt v1.3.8 // indirect golang.org/x/crypto v0.18.0 // indirect - golang.org/x/image v0.15.0 // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect diff --git a/go.sum b/go.sum index e4c9c316..60ead25b 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +fyne.io/systray v1.10.0 h1:Yr1D9Lxeiw3+vSuZWPlaHC8BMjIHZXJKkek706AfYQk= +fyne.io/systray v1.10.0/go.mod h1:oM2AQqGJ1AMo4nNqZFYU8xYygSBZkW2hmdJ7n4yjedE= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -42,6 +44,8 @@ github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435 h1:AnwbdEI8eV3GzLM3SlrJlYmYa6OB5X8RwY4A8QJOCP0= +github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435/go.mod h1:EMJ8XWTopp8OLRBMUm9vHE8Wn48CNpU21HM817OKNrc= github.com/dhaavi/winres v0.2.2 h1:SUago7FwhgLSMyDdeuV6enBZ+ZQSl0KwcnbWzvlfBls= github.com/dhaavi/winres v0.2.2/go.mod h1:1NTs+/DtKP1BplIL1+XQSoq4X1PUfLczexS7gf3x9T4= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -70,6 +74,9 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= +github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= @@ -354,6 +361,7 @@ golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/runtime/.gitkeep b/runtime/.gitkeep deleted file mode 100644 index 0d64ac96..00000000 --- a/runtime/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -The new portbase should land here. \ No newline at end of file From d524bce1661a7e0510d2367afcde11b63b2361ac Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Fri, 22 Mar 2024 11:45:18 +0100 Subject: [PATCH 05/35] Migrate tauri from portmaster-ui to desktop/tauri. Update build system --- .angulardoc.json | 4 + .earthlyignore | 12 +- .gitignore | 10 +- .vscode/settings.json | 1 + Earthfile | 261 +- desktop/angular/package.json | 2 +- desktop/tauri/.gitkeep | 0 desktop/tauri/assets | 1 + desktop/tauri/src-tauri/.gitignore | 3 + desktop/tauri/src-tauri/Cargo.lock | 7286 +++++++++++++++++ desktop/tauri/src-tauri/Cargo.toml | 75 + desktop/tauri/src-tauri/build.rs | 3 + desktop/tauri/src-tauri/src/main.rs | 204 + desktop/tauri/src-tauri/src/portapi/client.rs | 191 + .../tauri/src-tauri/src/portapi/message.rs | 258 + desktop/tauri/src-tauri/src/portapi/mod.rs | 4 + .../src-tauri/src/portapi/models/config.rs | 18 + .../tauri/src-tauri/src/portapi/models/mod.rs | 4 + .../src/portapi/models/notification.rs | 70 + .../tauri/src-tauri/src/portapi/models/spn.rs | 8 + .../src-tauri/src/portapi/models/subsystem.rs | 45 + desktop/tauri/src-tauri/src/portapi/types.rs | 199 + .../src-tauri/src/portmaster/commands.rs | 182 + desktop/tauri/src-tauri/src/portmaster/mod.rs | 294 + .../src-tauri/src/portmaster/notifications.rs | 103 + .../src-tauri/src/portmaster/websocket.rs | 45 + .../tauri/src-tauri/src/service/manager.rs | 17 + desktop/tauri/src-tauri/src/service/mod.rs | 76 + desktop/tauri/src-tauri/src/service/status.rs | 27 + .../tauri/src-tauri/src/service/systemd.rs | 246 + .../src-tauri/src/service/windows_service.rs | 167 + desktop/tauri/src-tauri/src/traymenu.rs | 344 + desktop/tauri/src-tauri/src/window.rs | 151 + desktop/tauri/src-tauri/src/xdg/mod.rs | 585 ++ desktop/tauri/src-tauri/tauri.conf.json | 106 + 35 files changed, 10960 insertions(+), 42 deletions(-) create mode 100644 .angulardoc.json create mode 100644 .vscode/settings.json delete mode 100644 desktop/tauri/.gitkeep create mode 120000 desktop/tauri/assets create mode 100644 desktop/tauri/src-tauri/.gitignore create mode 100644 desktop/tauri/src-tauri/Cargo.lock create mode 100644 desktop/tauri/src-tauri/Cargo.toml create mode 100644 desktop/tauri/src-tauri/build.rs create mode 100644 desktop/tauri/src-tauri/src/main.rs create mode 100644 desktop/tauri/src-tauri/src/portapi/client.rs create mode 100644 desktop/tauri/src-tauri/src/portapi/message.rs create mode 100644 desktop/tauri/src-tauri/src/portapi/mod.rs create mode 100644 desktop/tauri/src-tauri/src/portapi/models/config.rs create mode 100644 desktop/tauri/src-tauri/src/portapi/models/mod.rs create mode 100644 desktop/tauri/src-tauri/src/portapi/models/notification.rs create mode 100644 desktop/tauri/src-tauri/src/portapi/models/spn.rs create mode 100644 desktop/tauri/src-tauri/src/portapi/models/subsystem.rs create mode 100644 desktop/tauri/src-tauri/src/portapi/types.rs create mode 100644 desktop/tauri/src-tauri/src/portmaster/commands.rs create mode 100644 desktop/tauri/src-tauri/src/portmaster/mod.rs create mode 100644 desktop/tauri/src-tauri/src/portmaster/notifications.rs create mode 100644 desktop/tauri/src-tauri/src/portmaster/websocket.rs create mode 100644 desktop/tauri/src-tauri/src/service/manager.rs create mode 100644 desktop/tauri/src-tauri/src/service/mod.rs create mode 100644 desktop/tauri/src-tauri/src/service/status.rs create mode 100644 desktop/tauri/src-tauri/src/service/systemd.rs create mode 100644 desktop/tauri/src-tauri/src/service/windows_service.rs create mode 100644 desktop/tauri/src-tauri/src/traymenu.rs create mode 100644 desktop/tauri/src-tauri/src/window.rs create mode 100644 desktop/tauri/src-tauri/src/xdg/mod.rs create mode 100644 desktop/tauri/src-tauri/tauri.conf.json diff --git a/.angulardoc.json b/.angulardoc.json new file mode 100644 index 00000000..253388ca --- /dev/null +++ b/.angulardoc.json @@ -0,0 +1,4 @@ +{ + "repoId": "8f466ce7-4b75-4048-8b8a-cad5bf173aa0", + "lastSync": 0 +} \ No newline at end of file diff --git a/.earthlyignore b/.earthlyignore index 37c45b4d..06918d0b 100644 --- a/.earthlyignore +++ b/.earthlyignore @@ -12,4 +12,14 @@ desktop/angular/.angular # Assets are ignored here because the symlink wouldn't work in # the buildkit container so we copy the assets directly in Earthfile. -desktop/angular/assets \ No newline at end of file +desktop/angular/assets + + +desktop/tauri/src-tauri/target +.gitignore +AUTHORS +CODE_OF_CONDUCT.md +LICENSE +README.md +TESTING.md +TRADEMARKS \ No newline at end of file diff --git a/.gitignore b/.gitignore index 88f8a650..8268448f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,6 @@ # Compiled binaries -portmaster -portmaster.exe -dnsonly -dnsonly.exe -main -main.exe -integrationtest -integrationtest.exe +*.exe +dist/ # Dist dir dist diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/Earthfile b/Earthfile index c0da3562..ad1893d9 100644 --- a/Earthfile +++ b/Earthfile @@ -1,10 +1,23 @@ -VERSION --arg-scope-and-set 0.8 +VERSION --arg-scope-and-set --global-cache 0.8 ARG --global go_version = 1.21 ARG --global distro = alpine3.18 ARG --global node_version = 18 ARG --global outputDir = "./dist" +# The list of rust targets we support. They will be automatically converted +# to GOOS, GOARCH and GOARM when building go binaries. See the +RUST_TO_GO_ARCH_STRING +# helper method at the bottom of the file. +ARG --global architectures = "x86_64-unknown-linux-gnu" \ + "aarch64-unknown-linux-gnu" \ + "armv7-unknown-linux-gnueabihf" \ + "arm-unknown-linux-gnueabi" \ + "x86_64-pc-windows-gnu" + +# Import the earthly rust lib since it already provides some useful +# build-targets and methods to initialize the rust toolchain. +IMPORT github.com/earthly/lib/rust:3.0.2 AS rust + go-deps: FROM golang:${go_version}-${distro} WORKDIR /go-workdir @@ -24,7 +37,6 @@ go-deps: COPY go.sum . RUN go mod download - go-base: FROM +go-deps @@ -42,6 +54,15 @@ go-base: # ./assets COPY assets ./assets +# updates all go dependencies and runs go mod tidy, saving go.mod and go.sum locally. +update-go-deps: + FROM +go-base + + RUN go get -u ./.. + RUN go mod tidy + SAVE ARTIFACT go.mod AS LOCAL go.mod + SAVE ARTIFACT --if-exists go.sum AS LOCAL go.sum + # mod-tidy runs 'go mod tidy', saving go.mod and go.sum locally. mod-tidy: FROM +go-base @@ -61,6 +82,9 @@ build-go: ARG GOARM ARG CMDS=portmaster-start portmaster-core hub notifier + # Get the current version + DO +GET_VERSION + CACHE --sharing shared "$GOCACHE" CACHE --sharing shared "$GOMODCACHE" @@ -73,20 +97,17 @@ build-go: # Build all go binaries from the specified in CMDS FOR bin IN $CMDS - RUN go build -o "/tmp/build/" ./cmds/${bin} + RUN go build -o "/tmp/build/" ./cmds/${bin} END - LET NAME = "" - FOR bin IN $(ls -1 "/tmp/build/") - SET NAME = "${outputDir}/${GOOS}_${GOARCH}/${bin}" - IF [ "${GOARM}" != "" ] - SET NAME = "${outputDir}/${GOOS}_${GOARCH}v${GOARM}/${bin}" - END + DO +GO_ARCH_STRING --goos="${GOOS}" --goarch="${GOARCH}" --goarm="${GOARM}" - SAVE ARTIFACT "/tmp/build/${bin}" AS LOCAL "${NAME}" + SAVE ARTIFACT "/tmp/build/${bin}" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/${bin}" END + SAVE ARTIFACT "/tmp/build/" ./output + # Test one or more go packages. # Run `earthly +test-go` to test all packages # Run `earthly +test-go --PKG="service/firewall"` to only test a specific package. @@ -108,29 +129,19 @@ test-go: END test-go-all-platforms: - # Linux platforms: - BUILD +test-go --GOARCH=amd64 --GOOS=linux - BUILD +test-go --GOARCH=arm64 --GOOS=linux - BUILD +test-go --GOARCH=arm --GOOS=linux --GOARM=5 - BUILD +test-go --GOARCH=arm --GOOS=linux --GOARM=6 - BUILD +test-go --GOARCH=arm --GOOS=linux --GOARM=7 - - # Windows platforms: - BUILD +test-go --GOARCH=amd64 --GOOS=windows - BUILD +test-go --GOARCH=arm64 --GOOS=windows + LOCALLY + FOR arch IN ${architectures} + DO +RUST_TO_GO_ARCH_STRING --rustTarget="${arch}" + BUILD +test-go --GOARCH="${GOARCH}" --GOOS="${GOOS}" --GOARM="${GOARM}" + END # Builds portmaster-start, portmaster-core, hub and notifier for all supported platforms build-go-release: - # Linux platforms: - BUILD +build-go --GOARCH=amd64 --GOOS=linux - BUILD +build-go --GOARCH=arm64 --GOOS=linux - BUILD +build-go --GOARCH=arm --GOOS=linux --GOARM=5 - BUILD +build-go --GOARCH=arm --GOOS=linux --GOARM=6 - BUILD +build-go --GOARCH=arm --GOOS=linux --GOARM=7 - - # Windows platforms: - BUILD +build-go --GOARCH=amd64 --GOOS=windows - BUILD +build-go --GOARCH=arm64 --GOOS=windows + LOCALLY + FOR arch IN ${architectures} + DO +RUST_TO_GO_ARCH_STRING --rustTarget="${arch}" + BUILD +build-go --GOARCH="${GOARCH}" --GOOS="${GOOS}" --GOARM="${GOARM}" + END # Builds all binaries from the cmds/ folder for linux/windows AMD64 # Most utility binaries are never needed on other platforms. @@ -187,13 +198,199 @@ angular-project: # Build the angular projects (portmaster-UI and tauri-builtin) in production mode angular-release: BUILD +angular-project --project=portmaster --dist=./dist --configuration=production --baseHref=/ui/modules/portmaster - BUILD +angular-project --project=tauri-builtin --dist=./dist/tauri-builtin --configuration=production --baseHref="/" # Build the angular projects (portmaster-UI and tauri-builtin) in dev mode angular-dev: BUILD +angular-project --project=portmaster --dist=./dist --configuration=development --baseHref=/ui/modules/portmaster - BUILD +angular-project --project=tauri-builtin --dist=./dist/tauri-builtin --configuration=development --baseHref="/" + +# A base target for rust to prepare the build container +rust-base: + FROM rust:1.76-bookworm + + RUN apt-get update -qq + RUN apt-get install --no-install-recommends -qq \ + autoconf \ + autotools-dev \ + libtool-bin \ + clang \ + cmake \ + bsdmainutils \ + g++-mingw-w64-x86-64 \ + gcc-aarch64-linux-gnu \ + gcc-arm-none-eabi \ + gcc-arm-linux-gnueabi \ + gcc-arm-linux-gnueabihf \ + libgtk-3-dev \ + libjavascriptcoregtk-4.1-dev \ + libsoup-3.0-dev \ + libwebkit2gtk-4.1-dev \ + build-essential \ + curl \ + wget \ + file \ + libssl-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev + + # Add some required rustup components + RUN rustup component add clippy + RUN rustup component add rustfmt + + # Install toolchains and targets + FOR arch IN ${architectures} + RUN rustup target add ${arch} + END + + DO rust+INIT --keep_fingerprints=true + + # For now we need tauri-cli 1.5 for bulding + DO rust+CARGO --args="install tauri-cli --version ^1.5.11" + +tauri-src: + FROM +rust-base + + WORKDIR /app/tauri + + # --keep-ts is necessary to ensure that the timestamps of the source files + # are preserved such that Rust's incremental compilation works correctly. + COPY --keep-ts ./desktop/tauri/ . + COPY assets/data ./assets + COPY (+angular-project/dist/tauri-builtin --project=tauri-builtin --dist=./dist/tauri-builtin --configuration=production --baseHref="/") ./../angular/dist/tauri-builtin + +build-tauri: + FROM +tauri-src + + ARG --required target + ARG output="release/[^\./]+" + ARG bundle="none" + + # if we want tauri to create the installer bundles we also need to provide all external binaries + # we need to do some magic here because tauri expects the binaries to include the rust target tripple. + # We already knwo that triple because it's a required argument. From that triple, we use +RUST_TO_GO_ARCH_STRING + # function from below to parse the triple and guess wich GOOS and GOARCH we need. + IF [ "${bundle}" != "none" ] + RUN mkdir /tmp/gobuild + RUN mkdir ./binaries + + DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}" + RUN echo "GOOS=${GOOS} GOARCH=${GOARCH} GOARM=${GOARM} GO_ARCH_STRING=${GO_ARCH_STRING}" + + COPY (+build-go/output --GOOS="${GOOS}" --CMDS="portmaster-start portmaster-core" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild + + LET dest="" + FOR bin IN $(ls /tmp/gobuild) + SET dest="./binaries/${bin}-${target}" + + IF [ -z "${bin##*.exe}" ] + SET dest = "./binaries/${bin%.*}-${target}.exe" + END + + RUN echo "Copying ${bin} to ${dest}" + RUN cp "/tmp/gobuild/${bin}" "${dest}" + END + END + + # The following is exected to work but doesn't. for whatever reason cargo-sweep errors out on the windows-toolchain. + # + # DO rust+CARGO --args="tauri build --bundles none --ci --target=${target}" --output="release/[^/\.]+" + # + # For, now, we just directly mount the rust target cache and call cargo ourself. + + DO rust+SET_CACHE_MOUNTS_ENV + RUN --mount=$EARTHLY_RUST_TARGET_CACHE cargo tauri build --bundles "${bundle}" --ci --target="${target}" + DO rust+COPY_OUTPUT --output="${output}" + + RUN ls target + +tauri-release: + LOCALLY + + ARG bundle="none" + + FOR arch IN ${architectures} + BUILD +build-tauri --target="${arch}" --bundle="${bundle}" + END release: BUILD +build-go-release BUILD +angular-release + + +# Takes GOOS, GOARCH and optionally GOARM and creates a string representation for file-names. +# in the form of ${GOOS}_{GOARCH} if GOARM is empty, otherwise ${GOOS}_${GOARCH}v${GOARM}. +# Thats the same format as expected and served by our update server. +# +# The result is available as GO_ARCH_STRING environment variable in the build context. +GO_ARCH_STRING: + FUNCTION + ARG --required goos + ARG --required goarch + ARG goarm + + LET result = "${goos}_${goarch}" + IF [ "${goarm}" != "" ] + SET result = "${goos}_${goarch}v${goarm}" + END + + ENV GO_ARCH_STRING="${result}" + +# Takes a rust target (--rustTarget) and extracts architecture and OS and arm version +# and finally calls GO_ARCH_STRING. +# +# The result is available as GO_ARCH_STRING environment variable in the build context. +# It also exports GOOS, GOARCH and GOARM environment variables. +RUST_TO_GO_ARCH_STRING: + FUNCTION + ARG --required rustTarget + + LET goos="" + IF [ -z "${rustTarget##*linux*}" ] + SET goos="linux" + ELSE + SET goos="windows" + END + + + LET goarch="" + LET goarm="" + + IF [ -z "${rustTarget##*x86_64*}" ] + SET goarch="amd64" + ELSE IF [ -z "${rustTarget##*arm*}" ] + SET goarch="arm" + SET goarm="6" + + IF [ -z "${rustTarget##*v7*}" ] + SET goarm="7" + END + ELSE IF [ -z "${rustTarget##*aarch64*}" ] + SET goarch="arm64" + ELSE + RUN echo "GOARCH not detected"; \ + exit 1; + END + + ENV GOOS="${goos}" + ENV GOARCH="${goarch}" + ENV GOARM="${goarm}" + + DO +GO_ARCH_STRING --goos="${goos}" --goarch="${goarch}" --goarm="${goarm}" + +GET_VERSION: + FUNCTION + LOCALLY + + LET VERSION=$(git tag --points-at) + IF [ -z "${VERSION}"] + SET VERSION=$(git describe --tags --abbrev=0)§dev§build + ELSE IF ! git diff --quite + SET VERSION="${VERSION}§dev§build" + END + + RUN echo "Version is ${VERSION}" + ENV VERSION="${VERSION}" + +test: + LOCALLY + + DO +GET_VERSION \ No newline at end of file diff --git a/desktop/angular/package.json b/desktop/angular/package.json index 4131e18f..21207615 100644 --- a/desktop/angular/package.json +++ b/desktop/angular/package.json @@ -15,7 +15,7 @@ "chrome-extension": "NODE_ENV=production ng build --configuration production portmaster-chrome-extension", "chrome-extension:dev": "ng build --configuration development portmaster-chrome-extension --watch", "build": "npm run build-libs && NODE_ENV=production ng build --configuration production --base-href /ui/modules/portmaster/", - "build-tauri": "npm run build-libs && NODE_ENV=production ng build --configuration production", + "build-tauri": "npm run build-libs && NODE_ENV=production ng build --configuration production tauri-builtin", "serve-tauri-builtin": "ng serve tauri-builtin --port 4100", "serve-app": "ng serve --port 4200 --proxy-config ./proxy.json", "tauri-dev": "npm install && run-s build-libs:dev && run-p serve-app serve-tauri-builtin" diff --git a/desktop/tauri/.gitkeep b/desktop/tauri/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/desktop/tauri/assets b/desktop/tauri/assets new file mode 120000 index 00000000..21dab851 --- /dev/null +++ b/desktop/tauri/assets @@ -0,0 +1 @@ +../../assets/data \ No newline at end of file diff --git a/desktop/tauri/src-tauri/.gitignore b/desktop/tauri/src-tauri/.gitignore new file mode 100644 index 00000000..aba21e24 --- /dev/null +++ b/desktop/tauri/src-tauri/.gitignore @@ -0,0 +1,3 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ diff --git a/desktop/tauri/src-tauri/Cargo.lock b/desktop/tauri/src-tauri/Cargo.lock new file mode 100644 index 00000000..a0bda3a8 --- /dev/null +++ b/desktop/tauri/src-tauri/Cargo.lock @@ -0,0 +1,7286 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom 0.2.11", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anstream" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "app" +version = "0.1.0" +dependencies = [ + "assert_matches", + "cached", + "ctor", + "dataurl", + "dirs", + "futures-util", + "gdk-pixbuf", + "gdk-pixbuf-sys", + "gio-sys 0.18.1", + "glib 0.18.4", + "glib-sys 0.18.1", + "gtk", + "gtk-sys", + "http 1.0.0", + "lazy_static", + "log", + "notify-rust", + "pretty_env_logger", + "rust-ini", + "serde", + "serde_json", + "sha", + "tauri", + "tauri-build", + "tauri-cli", + "tauri-plugin-cli", + "tauri-plugin-clipboard-manager", + "tauri-plugin-dialog", + "tauri-plugin-notification", + "tauri-plugin-os", + "tauri-plugin-shell", + "tauri-plugin-single-instance", + "thiserror", + "tokio", + "tokio-websockets", + "url", + "uuid", + "which", + "windows 0.54.0", + "windows-service", +] + +[[package]] +name = "ar" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d67af77d68a931ecd5cbd8a3b5987d63a1d1d1278f7f6a60ae33db485cdebb69" + +[[package]] +name = "arboard" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aafb29b107435aa276664c1db8954ac27a6e105cdad3c88287a199eb0e313c08" +dependencies = [ + "clipboard-win", + "core-graphics 0.22.3", + "image", + "log", + "objc", + "objc-foundation", + "objc_id", + "parking_lot", + "thiserror", + "winapi", + "x11rb", +] + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "async-broadcast" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" +dependencies = [ + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" +dependencies = [ + "concurrent-queue", + "event-listener 4.0.0", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" +dependencies = [ + "async-lock 3.2.0", + "async-task", + "concurrent-queue", + "fastrand 2.0.1", + "futures-lite 2.1.0", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "blocking", + "futures-lite 1.13.0", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.27", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6afaa937395a620e33dc6a742c593c01aced20aa376ffb0f628121198578ccc7" +dependencies = [ + "async-lock 3.2.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.1.0", + "parking", + "polling 3.3.1", + "rustix 0.38.30", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" +dependencies = [ + "event-listener 4.0.0", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" +dependencies = [ + "async-io 1.13.0", + "async-lock 2.8.0", + "async-signal", + "blocking", + "cfg-if", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.30", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-recursion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "async-signal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" +dependencies = [ + "async-io 2.2.2", + "async-lock 2.8.0", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.30", + "signal-hook-registry", + "slab", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-task" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" + +[[package]] +name = "async-trait" +version = "0.1.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "atk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4af014b17dd80e8af9fa689b2d4a211ddba6eb583c1622f35d0cb543f6b17e4" +dependencies = [ + "atk-sys", + "glib 0.18.4", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "base64 0.21.5", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http 0.2.11", + "http-body", + "hyper", + "itoa 1.0.10", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 0.2.11", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +dependencies = [ + "serde", +] + +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +dependencies = [ + "async-channel", + "async-lock 3.2.0", + "async-task", + "fastrand 2.0.1", + "futures-io", + "futures-lite 2.1.0", + "piper", + "tracing", +] + +[[package]] +name = "brotli" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bswap" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3acc5ce9c60e68df21b877f13f908ef95c89f01cb6c656cf76ba95f10bc72f5" + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytecount" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" + +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cached" +version = "0.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c8c50262271cdf5abc979a5f76515c234e764fa025d1ba4862c0f0bcda0e95" +dependencies = [ + "ahash", + "cached_proc_macro", + "cached_proc_macro_types", + "hashbrown 0.14.3", + "instant", + "once_cell", + "thiserror", +] + +[[package]] +name = "cached_proc_macro" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c878c71c2821aa2058722038a59a67583a4240524687c6028571c9b395ded61f" +dependencies = [ + "darling 0.14.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" + +[[package]] +name = "cairo-rs" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f33613627f0dea6a731b0605101fad59ba4f193a52c96c4687728d822605a8a1" +dependencies = [ + "bitflags 2.4.1", + "cairo-sys-rs", + "glib 0.18.4", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys 0.18.1", + "libc", + "system-deps", +] + +[[package]] +name = "cargo_toml" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d1ece59890e746567b467253aea0adbe8a21784d0b025d8a306f66c391c2957" +dependencies = [ + "serde", + "toml 0.8.2", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.48.5", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap" +version = "4.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.10.0", +] + +[[package]] +name = "clap_complete" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "885e4d7d5af40bfb99ae6f9433e292feac98d452dcb3ec3d25dfe7552b77da8c" +dependencies = [ + "clap 4.4.11", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "clipboard-win" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics 0.23.1", + "foreign-types 0.5.0", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "common-path" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2382f75942f4b3be3690fe4f86365e9c853c1587d6ee58212cebf6e2a9ccd101" + +[[package]] +name = "concurrent-queue" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + +[[package]] +name = "const-random" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaf16c9c2c612020bcfd042e170f6e32de9b9d75adb5277cdbbd2e2c8c8299a" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.11", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset 0.9.0", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.52", +] + +[[package]] +name = "ctor" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e" +dependencies = [ + "quote", + "syn 2.0.52", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctrlc" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b467862cc8610ca6fc9a1532d7777cee0804e678ab45410897b9396495994a0b" +dependencies = [ + "nix 0.27.1", + "windows-sys 0.52.0", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core 0.20.3", + "darling_macro 0.20.3", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 2.0.52", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core 0.20.3", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + +[[package]] +name = "dataurl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17a1f14ed857323d318ca723a05a456196347efbe855f712f68cf6b8a14f8f15" +dependencies = [ + "atty", + "base64 0.13.1", + "clap 2.34.0", + "encoding_rs", + "percent-encoding", + "url", +] + +[[package]] +name = "deranged" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" +dependencies = [ + "libc", + "redox_users 0.3.5", + "winapi", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.4", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "embed-resource" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54cc3e827ee1c3812239a9a41dede7b4d7d5d5464faa32d71bd7cba28ce2cb2" +dependencies = [ + "cc", + "rustc_version", + "toml 0.8.2", + "vswhom", + "winreg 0.51.0", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enumflags2" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5998b4f30320c9d93aed72f63af821bfdac50465b75428fce77b48ec482c3939" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "erased-serde" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b73807008a3c7f171cc40312f37d95ef0396e048b5848d775f54b1a4dd4a0d3" +dependencies = [ + "serde", +] + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.0", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fdeflate" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset 0.9.0", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fraction" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3027ae1df8d41b4bed2241c8fdad4acc1e7af60c8e17743534b545e77182d678" +dependencies = [ + "lazy_static", + "num", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" + +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" +dependencies = [ + "fastrand 2.0.1", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "futures-sink" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" + +[[package]] +name = "futures-util" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5ba081bdef3b75ebcdbfc953699ed2d7417d6bd853347a42a37d76406a33646" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib 0.18.4", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446f32b74d22c33b7b258d4af4ffde53c2bf96ca2e29abdf1a785fe59bd6c82c" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib 0.18.4", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90fbf5c033c65d93792192a49a8efb5bb1e640c419682a58bb96f5ae77f3d4a" +dependencies = [ + "gdk-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2ea8a4909d530f79921290389cbd7c34cb9d623bfe970eaae65ca5f9cd9cce" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib 0.18.4", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee8f00f4ee46cad2939b8990f5c70c94ff882c3028f3cc5abf950fa4ab53043" +dependencies = [ + "gdk-sys", + "glib-sys 0.18.1", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys 0.18.1", + "glib 0.18.4", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b693b8e39d042a95547fc258a7b07349b1f0b48f4b2fa3108ba3c51c0b5229" +dependencies = [ + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16aa2475c9debed5a32832cb5ff2af5a3f9e1ab9e69df58eaadc1ab2004d6eba" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys 0.16.3", + "glib-macros 0.16.8", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951bbd7fdc5c044ede9f05170f05a3ae9479239c3afdfe2d22d537a3add15c4e" +dependencies = [ + "bitflags 2.4.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys 0.18.1", + "glib-macros 0.18.3", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1a9325847aa46f1e96ffea37611b9d51fc4827e67f79e7de502a297560a67b" +dependencies = [ + "anyhow", + "heck", + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "glib-macros" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72793962ceece3863c2965d7f10c8786323b17c7adea75a515809fa20ab799a5" +dependencies = [ + "heck", + "proc-macro-crate 2.0.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "glib-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "gobject-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" +dependencies = [ + "glib-sys 0.16.3", + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys 0.18.1", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93c4f5e0e20b60e10631a5f06da7fe3dda744b05ad0ea71fee2f47adf865890c" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib 0.18.4", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "h2" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.11", + "indexmap 2.1.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "handlebars" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa67bab9ff362228eb3d00bd024a4965d8231bbb7921167f0cfa66c6626b225" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.10", +] + +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.10", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.11", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.11", + "http-body", + "httparse", + "httpdate", + "itoa 1.0.10", + "pin-project-lite", + "socket2 0.4.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.51.1", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "ignore" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.3", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", + "qoi", + "tiff", +] + +[[package]] +name = "include_dir" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", + "serde", +] + +[[package]] +name = "infer" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f551f8c3a39f68f986517db0d1759de85881894fdc7db798bd2a9df9cb04b7fc" +dependencies = [ + "cfb", +] + +[[package]] +name = "infer" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb33622da908807a06f9513c19b3c1ad50fab3e4137d82a78107d502075aa199" +dependencies = [ + "cfb", +] + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.3", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi 0.3.3", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "iso8601" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" +dependencies = [ + "nom", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib 0.18.4", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff1e1486799e3f64129f8ccad108b38290df9cd7015cd31bed17239f0789d6" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "treediff", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "jsonschema" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a071f4f7efc9a9118dfb627a0a94ef247986e1ab8606a4c806ae2b3aa3b6978" +dependencies = [ + "ahash", + "anyhow", + "base64 0.21.5", + "bytecount", + "clap 4.4.11", + "fancy-regex", + "fraction", + "getrandom 0.2.11", + "iso8601", + "itoa 1.0.10", + "memchr", + "num-cmp", + "once_cell", + "parking_lot", + "percent-encoding", + "regex", + "reqwest", + "serde", + "serde_json", + "time", + "url", + "uuid", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.4.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "kuchikiki" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 1.9.3", + "matches", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib 0.18.4", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall 0.4.1", +] + +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +dependencies = [ + "value-bag", +] + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mac-notification-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51fca4d74ff9dbaac16a01b924bc3693fa2bba0862c2c633abc73f9a8ea21f64" +dependencies = [ + "cc", + "dirs-next", + "objc-foundation", + "objc_id", + "time", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "minisign" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23ef13ff1d745b1e52397daaa247e333c607f3cff96d4df2b798dc252db974b" +dependencies = [ + "getrandom 0.2.11", + "rpassword", + "scrypt", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "muda" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b564d551449738387fb4541aef5fbfceaa81b2b732f2534c1c7c89dc7d673eaa" +dependencies = [ + "cocoa", + "crossbeam-channel", + "gtk", + "keyboard-types", + "objc", + "once_cell", + "png", + "serde", + "thiserror", + "windows-sys 0.52.0", +] + +[[package]] +name = "ndk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +dependencies = [ + "bitflags 1.3.2", + "jni-sys", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.4.1+23.1.7779620" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "libc", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.4.1", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "notify-debouncer-mini" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43" +dependencies = [ + "crossbeam-channel", + "log", + "notify", +] + +[[package]] +name = "notify-rust" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "827c5edfa80235ded4ab3fe8e9dc619b4f866ef16fe9b1c6b8a7f8692c0f2226" +dependencies = [ + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.3", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "open" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a083c0c7e5e4a8ec4176346cf61f67ac674e8bfb059d9226e1c54a96b377c12" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-multimap" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4d6a8c22fc714f0c2373e6091bf6f5e9b37b1bc0b1184874b7e0a4e303d318f" +dependencies = [ + "dlv-list", + "hashbrown 0.14.3", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_info" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" +dependencies = [ + "log", + "serde", + "winapi", +] + +[[package]] +name = "os_pipe" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae859aa07428ca9a929b936690f8b12dc5f11dd8c6992a18ca93919f28bc177" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib 0.18.4", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "pest_meta" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "plist" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" +dependencies = [ + "base64 0.21.5", + "indexmap 2.1.0", + "line-wrap", + "quick-xml 0.31.0", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf63fa624ab313c11656b4cda960bfc46c410187ad493c41f6ba2d8c1e991c9e" +dependencies = [ + "cfg-if", + "concurrent-queue", + "pin-project-lite", + "rustix 0.38.30", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "pretty_env_logger" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.11", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "rayon" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +dependencies = [ + "getrandom 0.1.16", + "redox_syscall 0.1.57", + "rust-argon2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom 0.2.11", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "reqwest" +version = "0.11.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +dependencies = [ + "base64 0.21.5", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.11", + "http-body", + "hyper", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "winreg 0.50.0", +] + +[[package]] +name = "rfd" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241a0deb168c88050d872294f7b3106c1dfa8740942bcc97bc91b98e97b5c501" +dependencies = [ + "block", + "dispatch", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "gtk-sys", + "js-sys", + "log", + "objc", + "objc-foundation", + "objc_id", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom 0.2.11", + "libc", + "spin", + "untrusted", + "windows-sys 0.48.0", +] + +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.48.0", +] + +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64 0.13.1", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "rust-ini" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys 0.4.12", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6b63262c9fcac8659abfaa96cac103d28166d3ff3eaf8f412e19f3ae9e5a48" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7673e0aa20ee4937c6aacfc12bb8341cfbf054cdd21df6bec5fd0629fe9339b" + +[[package]] +name = "rustls-webpki" +version = "0.102.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de2635c8bc2b88d367767c5de8ea1d8db9af3f6219eba28442242d9ab81d1b89" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "schemars_derive" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2 0.12.2", + "salsa20", + "sha2", +] + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde_fmt" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa 1.0.10", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa 1.0.10", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "serde_spanned" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.10", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" +dependencies = [ + "base64 0.21.5", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.1.0", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" +dependencies = [ + "darling 0.20.3", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4208d5a903276a9f3b797afdf6c5bc12a8da1344b053b100abf3565ecc80cb7e" +dependencies = [ + "bswap", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared_child" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib 0.18.4", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "state" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" +dependencies = [ + "loom", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "sval" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a2386bea23a121e4e72450306b1dd01078b6399af11b93897bf84640a28a59" + +[[package]] +name = "sval_buffer" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16c047898a0e19002005512243bc9ef1c1037aad7d03d6c594e234efec80795" +dependencies = [ + "sval", + "sval_ref", +] + +[[package]] +name = "sval_dynamic" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74fb116e2ecdcb280b0108aa2ee4434df50606c3208c47ac95432730eaac20c" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10837b4f0feccef271b2b1c03784e08f6d0bb6d23272ec9e8c777bfadbb8f1b8" +dependencies = [ + "itoa 1.0.10", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891f5ecdf34ce61a8ab2d10f9cfdc303347b0afec4dad6702757419d2d8312a9" +dependencies = [ + "itoa 1.0.10", + "ryu", + "sval", +] + +[[package]] +name = "sval_nested" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63fcffb4b79c531f38e3090788b64f3f4d54a180aacf02d69c42fa4e4bf284c3" +dependencies = [ + "sval", + "sval_buffer", + "sval_ref", +] + +[[package]] +name = "sval_ref" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af725f9c2aa7cec4ca9c47da2cc90920c4c82d3fa537094c66c77a5459f5809d" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7589c649a03d21df40b9a926787d2c64937fa1dccec8d87c6cd82989a2e0a4" +dependencies = [ + "serde", + "sval", + "sval_nested", +] + +[[package]] +name = "swift-rs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bbdb58577b6301f8d17ae2561f32002a5bae056d444e0f69e611e504a276204" +dependencies = [ + "base64 0.21.5", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sys-locale" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0" +dependencies = [ + "libc", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c0dff18fed076d29cb5779e918ef4b8a5dbb756204e4a027794f0bce233d949" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cocoa", + "core-foundation", + "core-graphics 0.23.1", + "crossbeam-channel", + "dispatch", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "image", + "instant", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc", + "once_cell", + "parking_lot", + "png", + "raw-window-handle", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.52.0", + "windows-implement", + "windows-version", + "x11-dl", + "zbus", +] + +[[package]] +name = "tao-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec114582505d158b669b136e6851f85840c109819d77c42bb7c0709f727d18c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" + +[[package]] +name = "tauri" +version = "2.0.0-alpha.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05fb63873c39d3fd5ddad995d395e7b7394ece0b69aeacb31e91d24af48f3de1" +dependencies = [ + "anyhow", + "bytes", + "cocoa", + "dirs-next", + "embed_plist", + "futures-util", + "getrandom 0.2.11", + "glob", + "gtk", + "heck", + "http 0.2.11", + "ico", + "infer 0.15.0", + "jni", + "libc", + "log", + "mime", + "muda", + "objc", + "percent-encoding", + "png", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "state", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils 2.0.0-alpha.12", + "thiserror", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows 0.52.0", +] + +[[package]] +name = "tauri-build" +version = "2.0.0-alpha.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a2582ffb43e5c28932c43ffc40c295a9196a9a33ffb1163269c6baed84834a" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs-next", + "heck", + "json-patch", + "plist", + "semver", + "serde", + "serde_json", + "swift-rs", + "tauri-utils 2.0.0-alpha.12", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-bundler" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "657d0d0b2e820978ee51109bd75f03cee23dc1a83388d08f82fd368635c14e04" +dependencies = [ + "anyhow", + "ar", + "dirs-next", + "dunce", + "flate2", + "glob", + "handlebars", + "heck", + "hex", + "image", + "log", + "md5", + "os_pipe", + "plist", + "regex", + "semver", + "serde", + "serde_json", + "sha1", + "sha2", + "strsim 0.10.0", + "tar", + "tauri-icns", + "tauri-utils 1.5.3", + "tempfile", + "thiserror", + "time", + "ureq", + "uuid", + "walkdir", + "windows-sys 0.48.0", + "winreg 0.51.0", + "zip", +] + +[[package]] +name = "tauri-cli" +version = "1.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b2a88fd572b14c0ca224675aaad590e38d95e53846df55459edbdc910795eb" +dependencies = [ + "anyhow", + "axum", + "base64 0.21.5", + "cc", + "clap 4.4.11", + "clap_complete", + "colored", + "common-path", + "ctrlc", + "dialoguer", + "env_logger", + "glob", + "handlebars", + "heck", + "html5ever", + "ignore", + "image", + "include_dir", + "itertools", + "json-patch", + "jsonschema", + "kuchikiki", + "libc", + "log", + "minisign", + "notify", + "notify-debouncer-mini", + "once_cell", + "os_info", + "os_pipe", + "regex", + "semver", + "serde", + "serde-value", + "serde_json", + "shared_child", + "tauri-bundler", + "tauri-icns", + "tauri-utils 1.5.3", + "tokio", + "toml 0.8.2", + "toml_edit 0.21.1", + "unicode-width", + "ureq", + "url", + "winapi", + "zeroize", +] + +[[package]] +name = "tauri-codegen" +version = "2.0.0-alpha.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06976ec7b704d6b842169ffd4ce596e9ce45917a0ab462cb96a119fa2829be9" +dependencies = [ + "base64 0.21.5", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "tauri-utils 2.0.0-alpha.12", + "thiserror", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-icns" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b7eb4d0d43724ba9ba6a6717420ee68aee377816a3edbb45db8c18862b1431" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "tauri-macros" +version = "2.0.0-alpha.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff509be5a5ac34ec2e60d9029af1032c0a33e421f3e823bc92695192e2871c17" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.52", + "tauri-codegen", + "tauri-utils 2.0.0-alpha.12", +] + +[[package]] +name = "tauri-plugin-cli" +version = "2.0.0-alpha.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3ce9173dd3ae43c5c7529cce495e89cc9d8773adcd2a3e0efb5123aa052c64" +dependencies = [ + "clap 4.4.11", + "log", + "serde", + "serde_json", + "tauri", + "thiserror", +] + +[[package]] +name = "tauri-plugin-clipboard-manager" +version = "2.0.0-alpha.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32d537328bba01bcbbff4fc7daa1175744afdd42e554b6c897d9a1b1f76b023" +dependencies = [ + "arboard", + "log", + "serde", + "serde_json", + "tauri", + "tauri-build", + "thiserror", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.0.0-alpha.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ead9b1276ed45ffec0a27ff51239614fa9b462a7483f5cb98f0c555a40754e9" +dependencies = [ + "glib 0.16.9", + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-fs", + "thiserror", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.0.0-alpha.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c81479fdc92bab8609d896249e016404f9fac24a27ddf66e1daafd4db1a35" +dependencies = [ + "anyhow", + "glob", + "serde", + "tauri", + "thiserror", + "uuid", +] + +[[package]] +name = "tauri-plugin-notification" +version = "2.0.0-alpha.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e1cfe331495d0e72b9d48191eec98a54f9e189571b8ec6affb39b90b3df3bc" +dependencies = [ + "log", + "notify-rust", + "rand 0.8.5", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-build", + "thiserror", + "time", + "url", +] + +[[package]] +name = "tauri-plugin-os" +version = "2.0.0-alpha.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dfd82d59cdf0229ffe62d38e12bdfee053c4f915883afe6f982b672a7e28d44" +dependencies = [ + "gethostname 0.4.3", + "log", + "os_info", + "serde", + "serde_json", + "serialize-to-javascript", + "sys-locale", + "tauri", + "thiserror", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.0.0-alpha.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf84ccb3f5ac4df2dfeb5e2f09b9048d8633d9b98d72c701aba72642790f2d9" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "serde", + "serde_json", + "shared_child", + "tauri", + "thiserror", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.0.0-alpha.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d25b229dea0a7cb72ab43ebd17fa7479eda058678bead1ecca431013d5e5ebf" +dependencies = [ + "log", + "serde", + "serde_json", + "tauri", + "thiserror", + "windows-sys 0.52.0", + "zbus", +] + +[[package]] +name = "tauri-runtime" +version = "1.0.0-alpha.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a989e58af6e554dbac798a0a8d112faafc1509bcfab626466181e0724f09c5" +dependencies = [ + "gtk", + "http 0.2.11", + "jni", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils 2.0.0-alpha.12", + "thiserror", + "url", + "windows 0.52.0", +] + +[[package]] +name = "tauri-runtime-wry" +version = "1.0.0-alpha.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9f181a6f5f982204ae293c19f37ba90116b8ec0bfd0a08c7a7ba67200cd9e3" +dependencies = [ + "cocoa", + "gtk", + "http 0.2.11", + "jni", + "percent-encoding", + "raw-window-handle", + "tao", + "tauri-runtime", + "tauri-utils 2.0.0-alpha.12", + "webkit2gtk", + "webview2-com", + "windows 0.52.0", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ad0bbb31fccd1f4c56275d0a5c3abdf1f59999f72cb4ef8b79b4ed42082a21" +dependencies = [ + "aes-gcm", + "ctor", + "dunce", + "getrandom 0.2.11", + "glob", + "heck", + "html5ever", + "infer 0.13.0", + "json-patch", + "json5", + "kuchikiki", + "log", + "memchr", + "phf 0.11.2", + "schemars", + "semver", + "serde", + "serde_json", + "serde_with", + "serialize-to-javascript", + "thiserror", + "toml 0.7.8", + "url", + "walkdir", + "windows-version", +] + +[[package]] +name = "tauri-utils" +version = "2.0.0-alpha.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4858f99fc9f28b72008ef51d04d18b7e3646845c2bc18ee340045fed6ed5095" +dependencies = [ + "brotli", + "ctor", + "dunce", + "glob", + "heck", + "html5ever", + "infer 0.15.0", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.2", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "serde_with", + "thiserror", + "url", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb" +dependencies = [ + "embed-resource", + "toml 0.7.8", +] + +[[package]] +name = "tauri-winrt-notification" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006851c9ccefa3c38a7646b8cec804bb429def3da10497bfa977179869c3e8e2" +dependencies = [ + "quick-xml 0.30.0", + "windows 0.51.1", +] + +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand 2.0.1", + "redox_syscall 0.4.1", + "rustix 0.38.30", + "windows-sys 0.48.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tiff" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +dependencies = [ + "deranged", + "itoa 1.0.10", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2 0.5.5", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tokio-websockets" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b069bad86dda43d908b4221fe04fe49d2ed8e0a24d319a5c6a8d250e76fe15b" +dependencies = [ + "base64 0.21.5", + "bytes", + "futures-core", + "futures-sink", + "http 1.0.0", + "httparse", + "rand 0.8.5", + "ring", + "tokio", + "tokio-rustls", + "tokio-util", + "tracing", +] + +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap 2.1.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tray-icon" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad962d06d2bfd9b2ab4f665fc73b175523b834b1466a294520201c5845145f8" +dependencies = [ + "cocoa", + "core-graphics 0.23.1", + "crossbeam-channel", + "dirs-next", + "libappindicator", + "muda", + "objc", + "once_cell", + "png", + "serde", + "thiserror", + "windows-sys 0.52.0", +] + +[[package]] +name = "treediff" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303" +dependencies = [ + "serde_json", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 0.2.11", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset 0.9.0", + "tempfile", + "winapi", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f214ce18d8b2cbe84ed3aa6486ed3f5b285cf8d8fbdbce9f3f767a724adc35" +dependencies = [ + "base64 0.21.5", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "rustls-webpki", + "socks", + "url", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +dependencies = [ + "getrandom 0.2.11", + "sha1_smol", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "value-bag" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74797339c3b98616c009c7c3eb53a0ce41e85c8ec66bd3db96ed132d20cfdee8" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc35703541cbccb5278ef7b589d79439fc808ff0b5867195a3230f9a47421d39" +dependencies = [ + "erased-serde", + "serde", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "285b43c29d0b4c0e65aad24561baee67a1b69dc9be9375d4a85138cbf556f7f8" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "waker-fn" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.52", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + +[[package]] +name = "wasm-streams" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys 0.18.1", + "glib 0.18.4", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-roots" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ae9c7e420783826cf769d2c06ac9ba462f450eca5893bb8c6c6529a4e5dd33" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.52.0", + "windows-core 0.52.0", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "webview2-com-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ad85fceee6c42fa3d61239eba5a11401bf38407a849ed5ea1b407df08cca72" +dependencies = [ + "thiserror", + "windows 0.52.0", + "windows-core 0.52.0", +] + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "which" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.30", + "windows-sys 0.52.0", +] + +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-wsapoll" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af6abc2b9c56bd95887825a1ce56cde49a2a97c07e28db465d541f5098a2656c" +dependencies = [ + "cocoa", + "objc", + "raw-window-handle", + "windows-sys 0.52.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core 0.51.1", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-implement", + "windows-interface", + "windows-targets 0.52.4", +] + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result", + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-implement" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "windows-interface" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "windows-result" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd19df78e5168dfb0aedc343d1d1b8d422ab2db6756d2dc3fef75035402a3f64" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-service" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9db37ecb5b13762d95468a2fc6009d4b2c62801243223aabd44fca13ad13c8" +dependencies = [ + "bitflags 1.3.2", + "widestring", + "windows-sys 0.45.0", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", +] + +[[package]] +name = "windows-version" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75aa004c988e080ad34aff5739c39d0312f4684699d6d71fc8a198d057b8b9b4" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "winnow" +version = "0.5.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c830786f7720c2fd27a1a0e27a709dbd3c4d009b56d098fc742d4f4eab91fe2" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winreg" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "937f3df7948156640f46aacef17a70db0de5917bda9c92b0f751f3a955b588fc" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wry" +version = "0.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ad1bc1d6925e0cde1bd01830b0073cd0448e21357e843b9ede33b6d81c7423" +dependencies = [ + "base64 0.21.5", + "block", + "cfg_aliases", + "cocoa", + "core-graphics 0.23.1", + "crossbeam-channel", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http 0.2.11", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc", + "objc_id", + "once_cell", + "raw-window-handle", + "serde", + "serde_json", + "sha2", + "soup3", + "tao-macros", + "thiserror", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.52.0", + "windows-implement", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" +dependencies = [ + "gethostname 0.3.0", + "nix 0.26.4", + "winapi", + "winapi-wsapoll", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" +dependencies = [ + "nix 0.26.4", +] + +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys 0.4.12", + "rustix 0.38.30", +] + +[[package]] +name = "xdg-home" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" +dependencies = [ + "nix 0.26.4", + "winapi", +] + +[[package]] +name = "zbus" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31de390a2d872e4cd04edd71b425e29853f786dc99317ed72d73d6fcf5ebb948" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io 1.13.0", + "async-lock 2.8.0", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "byteorder", + "derivative", + "enumflags2", + "event-listener 2.5.3", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.26.4", + "once_cell", + "ordered-stream", + "rand 0.8.5", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d1794a946878c0e807f55a397187c11fc7a038ba5d868e7db4f3bd7760bc9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb80bb776dbda6e23d705cf0123c3b95df99c4ebeaec6c2599d4a5419902b4a9" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.7.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2 0.11.0", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.9+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zvariant" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] diff --git a/desktop/tauri/src-tauri/Cargo.toml b/desktop/tauri/src-tauri/Cargo.toml new file mode 100644 index 00000000..6fed0ec3 --- /dev/null +++ b/desktop/tauri/src-tauri/Cargo.toml @@ -0,0 +1,75 @@ +[package] +name = "app" +version = "0.1.0" +description = "Portmaster UI" +authors = ["Safing"] +license = "" +repository = "" +default-run = "app" +edition = "2021" +rust-version = "1.60" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +tauri-build = { version = "2.0.0-alpha", features = [] } + +[dependencies] +# Tauri +tauri = { version = "2.0.0-alpha", features = ["tray-icon", "icon-ico", "icon-png"] } +tauri-plugin-shell = "2.0.0-alpha" +tauri-plugin-dialog = "2.0.0-alpha" +tauri-plugin-clipboard-manager = "2.0.0-alpha" +tauri-plugin-os = "2.0.0-alpha" +tauri-plugin-single-instance = "2.0.0-alpha" +tauri-plugin-cli = "2.0.0-alpha" +tauri-plugin-notification = "2.0.0-alpha" + +# We still need the tauri-cli 1.5 for building +tauri-cli = "1.5.11" + +# General +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } +futures-util = { version = "0.3", features = ["sink"] } +dirs = "1.0" +rust-ini = "0.20.0" +dataurl = "0.1.2" +uuid = "1.6.1" +lazy_static = "1.4.0" +tokio = { version = "1.35.0", features = ["macros"] } +cached = "0.46.1" +notify-rust = "4.10.0" +assert_matches = "1.5.0" +tokio-websockets = { version = "0.5.0", features = ["client", "ring", "rand"] } +sha = "1.0.3" +http = "1.0.0" +url = "2.5.0" +thiserror = "1.0" +log = "0.4.21" +pretty_env_logger = "0.5.0" + +# Linux only +[target.'cfg(target_os = "linux")'.dependencies] +glib = "0.18.4" +gtk-sys = "0.18.0" +glib-sys = "0.18.1" +gdk-pixbuf = "0.18.3" +gdk-pixbuf-sys = "0.18.0" +gio-sys = "0.18.1" + +# Windows only +[target.'cfg(target_os = "windows")'.dependencies] +windows-service = "0.6.0" +windows = { version = "0.54.0", features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } + +[dev-dependencies] +which = "6.0.0" +gtk = "0.18" +ctor = "0.2.6" + +[features] +# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. +# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. +# DO NOT REMOVE!! +custom-protocol = [ "tauri/custom-protocol" ] diff --git a/desktop/tauri/src-tauri/build.rs b/desktop/tauri/src-tauri/build.rs new file mode 100644 index 00000000..795b9b7c --- /dev/null +++ b/desktop/tauri/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/desktop/tauri/src-tauri/src/main.rs b/desktop/tauri/src-tauri/src/main.rs new file mode 100644 index 00000000..2b1def20 --- /dev/null +++ b/desktop/tauri/src-tauri/src/main.rs @@ -0,0 +1,204 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use tauri::{AppHandle, Manager, RunEvent, WindowEvent}; +use tauri_plugin_cli::CliExt; + +// Library crates +mod portapi; +mod service; + +#[cfg(target_os = "linux")] +mod xdg; + +// App modules +mod portmaster; +mod traymenu; +mod window; + +use log::{debug, error, info, trace, warn}; +use portmaster::PortmasterExt; +use traymenu::setup_tray_menu; +use window::{close_splash_window, create_main_window}; + +#[macro_use] +extern crate lazy_static; + +#[derive(Clone, serde::Serialize)] +struct Payload { + args: Vec, + cwd: String, +} + +struct WsHandler { + handle: AppHandle, + background: bool, + + is_first_connect: bool, +} + +impl portmaster::Handler for WsHandler { + fn on_connect(&mut self, cli: portapi::client::PortAPI) -> () { + // we successfully connected to Portmaster. Set is_first_connect to false + // so we don't show the splash-screen when we loose connection. + self.is_first_connect = false; + + if let Err(err) = close_splash_window(&self.handle) { + error!("failed to close splash window: {}", err.to_string()); + } + + // create the main window now. It's not automatically visible by default. + // Rather, the angular application will show the window itself when it finished + // bootstrapping. + if let Err(err) = create_main_window(&self.handle) { + error!("failed to create main window: {}", err.to_string()); + } + + let handle = self.handle.clone(); + tauri::async_runtime::spawn(async move { + traymenu::tray_handler(cli, handle).await; + }); + } + + fn on_disconnect(&mut self) { + // if we're not running in background and this was the first connection attempt + // then display the splash-screen. + // + // Once we had a successful connection the splash-screen will not be shown anymore + // since there's already a main window with the angular application. + if !self.background && self.is_first_connect { + let _ = window::create_splash_window(&self.handle.clone()); + + self.is_first_connect = false + } + } +} + +fn main() { + pretty_env_logger::init(); + + let app = tauri::Builder::default() + // Shell plugin for open_external support + .plugin(tauri_plugin_shell::init()) + // Clipboard support + .plugin(tauri_plugin_clipboard_manager::init()) + // Dialog (Save/Open) support + .plugin(tauri_plugin_dialog::init()) + // OS Version and Architecture support + .plugin(tauri_plugin_os::init()) + // Single instance guard + .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| { + let _ = app.emit("single-instance", Payload { args: argv, cwd }); + })) + // Custom CLI arguments + .plugin(tauri_plugin_cli::init()) + // Notification support + .plugin(tauri_plugin_notification::init()) + // Our Portmaster Plugin that handles communication between tauri and our angular app. + .plugin(portmaster::init()) + // Setup the app an any listeners + .setup(|app| { + setup_tray_menu(app)?; + + // Setup the single-instance event listener that will create/focus the main window + // or the splash-screen. + let handle = app.handle().clone(); + app.listen_global("single-instance", move |_event| { + let _ = window::open_window(&handle); + }); + + // Handle cli flags: + // + let mut background = false; + match app.cli().matches() { + Ok(matches) => { + debug!("cli matches={:?}", matches); + + if let Some(bg_flag) = matches.args.get("background") { + match bg_flag.value.as_bool() { + Some(value) => { + background = value; + app.portmaster().set_show_after_bootstrap(!background); + } + None => {} + } + } + + if let Some(nf_flag) = matches.args.get("with-notifications") { + match nf_flag.value.as_bool() { + Some(v) => { + app.portmaster().with_notification_support(v); + } + None => {} + } + } + + if let Some(pf_flag) = matches.args.get("with-prompts") { + match pf_flag.value.as_bool() { + Some(v) => { + app.portmaster().with_connection_prompts(v); + } + None => {} + } + } + } + Err(err) => { + error!("failed to parse cli arguments: {}", err.to_string()); + } + }; + + // prepare a custom portmaster plugin handler that will show the splash-screen + // (if not in --background) and launch the tray-icon handler. + let handler = WsHandler { + handle: app.handle().clone(), + background, + is_first_connect: true, + }; + + // register the custom handler + app.portmaster().register_handler(handler); + + Ok(()) + }) + .any_thread() + .build(tauri::generate_context!()) + .expect("error while running tauri application"); + + app.run(|handle, e| match e { + RunEvent::WindowEvent { label, event, .. } => { + if label != "main" { + // We only have one window at most so any other label is unexpected + return; + } + + // Do not let the user close the window, instead send an event to the main + // window so we can show the "will not stop portmaster" dialog and let the window + // close itself using + // + // window.__TAURI__.window.getCurrent().close() + // + // Note: the above javascript does NOT trigger the CloseRequested event so + // there's no need to handle that case here. + // + match event { + WindowEvent::CloseRequested { api, .. } => { + debug!( + "window (label={}) close request received, forwarding to user-interface.", + label + ); + + api.prevent_close(); + if let Some(window) = handle.get_window(label.as_str()) { + let _ = window.emit("exit-requested", ""); + } + } + _ => {} + } + } + + RunEvent::ExitRequested { api, .. } => { + api.prevent_exit(); + } + _ => {} + }); +} diff --git a/desktop/tauri/src-tauri/src/portapi/client.rs b/desktop/tauri/src-tauri/src/portapi/client.rs new file mode 100644 index 00000000..b3b00d09 --- /dev/null +++ b/desktop/tauri/src-tauri/src/portapi/client.rs @@ -0,0 +1,191 @@ +use futures_util::{SinkExt, StreamExt}; +use http::Uri; +use log::{debug, error, warn}; +use std::collections::HashMap; +use std::sync::atomic::{AtomicUsize, Ordering}; +use tokio::sync::mpsc::{channel, Receiver, Sender}; +use tokio::sync::RwLock; +use tokio_websockets::{ClientBuilder, Error}; + +use super::message::*; +use super::types::*; + +/// An internal representation of a Command that +/// contains the PortAPI message as well as a response +/// channel that will receive all responses sent from the +/// server. +/// +/// Users should normally not need to use the Command struct +/// directly since `PortAPI` already abstracts the creation of +/// mpsc channels. +struct Command { + msg: Message, + response: Sender, +} + +/// The client implementation for PortAPI. +#[derive(Clone)] +pub struct PortAPI { + dispatch: Sender, +} + +/// The map type used to store message subscribers. +type SubscriberMap = RwLock>>; + +/// Connect to PortAPI at the specified URI. +/// +/// This method will launch a new async thread on the `tauri::async_runtime` +/// that will handle message to transmit and also multiplex server responses +/// to the appropriate subscriber. +pub async fn connect(uri: &str) -> Result { + let parsed = match uri.parse::() { + Ok(u) => u, + Err(_e) => { + return Err(Error::NoUriConfigured); // TODO(ppacher): fix the return error type. + } + }; + + let (mut client, _) = ClientBuilder::from_uri(parsed).connect().await?; + let (tx, mut dispatch) = channel::(64); + + tauri::async_runtime::spawn(async move { + let subscribers: SubscriberMap = RwLock::new(HashMap::new()); + let next_id = AtomicUsize::new(0); + + loop { + tokio::select! { + msg = client.next() => { + let msg = match msg { + Some(msg) => msg, + None => { + warn!("websocket connection lost"); + + dispatch.close(); + return; + } + }; + + match msg { + Err(err) => { + error!("failed to receive frame from websocket: {}", err); + + dispatch.close(); + return; + }, + Ok(msg) => { + let text = unsafe { + std::str::from_utf8_unchecked(msg.as_payload()) + }; + + match text.parse::() { + Ok(msg) => { + let id = msg.id; + let map = subscribers + .read() + .await; + + if let Some(sub) = map.get(&id) { + let res: Result = msg.try_into(); + match res { + Ok(response) => { + if let Err(err) = sub.send(response).await { + // The receiver side has been closed already, + // drop the read lock and remove the subscriber + // from our hashmap + drop(map); + + subscribers + .write() + .await + .remove(&id); + + debug!("subscriber for command {} closed read side: {}", id, err); + } + }, + Err(err) => { + error!("invalid command: {}", err); + } + } + } + }, + Err(err) => { + error!("failed to deserialize message: {}", err) + } + } + } + } + + }, + + Some(mut cmd) = dispatch.recv() => { + let id = next_id.fetch_add(1, Ordering::Relaxed); + cmd.msg.id = id; + let blob: String = cmd.msg.into(); + + debug!("Sending websocket frame: {}", blob); + + match client.send(tokio_websockets::Message::text(blob)).await { + Ok(_) => { + subscribers + .write() + .await + .insert(id, cmd.response); + }, + Err(err) => { + error!("failed to dispatch command: {}", err); + + // TODO(ppacher): we should send some error to cmd.response here. + // Otherwise, the sender of cmd might get stuck waiting for responses + // if they don't check for PortAPI.is_closed(). + + return + } + } + } + } + } + }); + + Ok(PortAPI { dispatch: tx }) +} + +impl PortAPI { + /// `request` sends a PortAPI `portapi::types::Request` to the server and returns a mpsc receiver channel + /// where all server responses are forwarded. + /// + /// If the caller does not intend to read any responses the returned receiver may be closed or + /// dropped. As soon as the async-thread launched in `connect` detects a closed receiver it is remove + /// from the subscription map. + /// + /// The default buffer size for the channel is 64. Use `request_with_buffer_size` to specify a dedicated buffer size. + pub async fn request( + &self, + r: Request, + ) -> std::result::Result, MessageError> { + self.request_with_buffer_size(r, 64).await + } + + // Like `request` but supports explicitly specifying a channel buffer size. + pub async fn request_with_buffer_size( + &self, + r: Request, + buffer: usize, + ) -> std::result::Result, MessageError> { + let (tx, rx) = channel(buffer); + + let msg: Message = r.try_into()?; + + let _ = self.dispatch.send(Command { response: tx, msg }).await; + + Ok(rx) + } + + /// Reports whether or not the websocket connection to the Portmaster Database API has been closed + /// due to errors. + /// + /// Users are expected to check this field on a regular interval to detect any issues and perform + /// a clean re-connect by calling `connect` again. + pub fn is_closed(&self) -> bool { + self.dispatch.is_closed() + } +} diff --git a/desktop/tauri/src-tauri/src/portapi/message.rs b/desktop/tauri/src-tauri/src/portapi/message.rs new file mode 100644 index 00000000..46eb7c77 --- /dev/null +++ b/desktop/tauri/src-tauri/src/portapi/message.rs @@ -0,0 +1,258 @@ +use thiserror::Error; + +/// MessageError describes any error that is encountered when parsing +/// PortAPI messages or when converting between the Request/Response types. +#[derive(Debug, Error)] +pub enum MessageError { + #[error("missing command id")] + MissingID, + + #[error("invalid command id")] + InvalidID, + + #[error("missing command")] + MissingCommand, + + #[error("missing key")] + MissingKey, + + #[error("missing payload")] + MissingPayload, + + #[error("unknown or unsupported command: {0}")] + UnknownCommand(String), + + #[error(transparent)] + InvalidPayload(#[from] serde_json::Error), +} + + +/// Payload defines the payload type and content of a PortAPI message. +/// +/// For the time being, only JSON payloads (indicated by a prefixed 'J' of the payload content) +/// is directly supported in `Payload::parse()`. +/// +/// For other payload types (like CBOR, BSON, ...) it's the user responsibility to figure out +/// appropriate decoding from the `Payload::UNKNOWN` variant. +#[derive(PartialEq, Debug, Clone)] +pub enum Payload { + JSON(String), + UNKNOWN(String), +} + +/// ParseError is returned from `Payload::parse()`. +#[derive(Debug, Error)] +pub enum ParseError { + #[error(transparent)] + JSON(#[from] serde_json::Error), + + #[error("unknown error while parsing")] + UNKNOWN +} + + +impl Payload { + /// Parse the payload into T. + /// + /// Only JSON parsing is supported for now. See [Payload] for more information. + pub fn parse<'a, T>(self: &'a Self) -> std::result::Result + where + T: serde::de::Deserialize<'a> { + + match self { + Payload::JSON(blob) => Ok(serde_json::from_str::(blob.as_str())?), + Payload::UNKNOWN(_) => Err(ParseError::UNKNOWN), + } + } +} + +/// Supports creating a Payload instance from a String. +/// +/// See [Payload] for more information. +impl std::convert::From for Payload { + fn from(value: String) -> Payload { + let mut chars = value.chars(); + let first = chars.next(); + let rest = chars.as_str().to_string(); + + match first { + Some(c) => match c { + 'J' => Payload::JSON(rest), + _ => Payload::UNKNOWN(value), + }, + None => Payload::UNKNOWN("".to_string()) + } + } +} + +/// Display implementation for Payload that just displays the raw payload. +impl std::fmt::Display for Payload { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Payload::JSON(payload) => { + write!(f, "J{}", payload) + }, + Payload::UNKNOWN(payload) => { + write!(f, "{}", payload) + } + } + } +} + +/// Message is an internal representation of a PortAPI message. +/// Users should more likely use `portapi::types::Request` and `portapi::types::Response` +/// instead of directly using `Message`. +/// +/// The struct is still public since it might be useful for debugging or to implement new +/// commands not yet supported by the `portapi::types` crate. +#[derive(PartialEq, Debug, Clone)] +pub struct Message { + pub id: usize, + pub cmd: String, + pub key: Option, + pub payload: Option, +} + +/// Implementation to marshal a PortAPI message into it's wire-format representation +/// (which is a string). +/// +/// Note that this conversion does not check for invalid messages! +impl std::convert::From for String { + fn from(value: Message) -> Self { + let mut result = "".to_owned(); + + result.push_str(value.id.to_string().as_str()); + result.push_str("|"); + result.push_str(&value.cmd); + + if let Some(key) = value.key { + result.push_str("|"); + result.push_str(key.as_str()); + } + + if let Some(payload) = value.payload { + result.push_str("|"); + result.push_str(payload.to_string().as_str()) + } + + result + } +} + +/// An implementation for `String::parse()` to convert a wire-format representation +/// of a PortAPI message to a Message instance. +/// +/// Any errors returned from `String::parse()` will be of type `MessageError` +impl std::str::FromStr for Message { + type Err = MessageError; + + fn from_str(line: &str) -> Result { + let parts = line.split("|").collect::>(); + + let id = match parts.get(0) { + Some(s) => match (*s).parse::() { + Ok(id) => Ok(id), + Err(_) => Err(MessageError::InvalidID), + }, + None => Err(MessageError::MissingID), + }?; + + let cmd = match parts.get(1) { + Some(s) => Ok(*s), + None => Err(MessageError::MissingCommand), + }? + .to_string(); + + let key = parts.get(2) + .and_then(|key| Some(key.to_string())); + + let payload : Option = parts.get(3) + .and_then(|p| Some(p.to_string().into())); + + return Ok(Message { + id, + cmd, + key, + payload: payload + }); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::Deserialize; + + #[derive(Debug, PartialEq, Deserialize)] + struct Test { + a: i64, + s: String, + } + + #[test] + fn payload_to_string() { + let p = Payload::JSON("{}".to_string()); + assert_eq!(p.to_string(), "J{}"); + + let p = Payload::UNKNOWN("some unknown content".to_string()); + assert_eq!(p.to_string(), "some unknown content"); + } + + #[test] + fn payload_from_string() { + let p: Payload = "J{}".to_string().into(); + assert_eq!(p, Payload::JSON("{}".to_string())); + + let p: Payload = "some unknown content".to_string().into(); + assert_eq!(p, Payload::UNKNOWN("some unknown content".to_string())); + } + + #[test] + fn payload_parse() { + let p: Payload = "J{\"a\": 100, \"s\": \"string\"}".to_string().into(); + + let t: Test = p.parse() + .expect("Expected payload parsing to work"); + + assert_eq!(t, Test{ + a: 100, + s: "string".to_string(), + }); + } + + #[test] + fn parse_message() { + let m = "10|insert|some:key|J{}".parse::() + .expect("Expected message to parse"); + + assert_eq!(m, Message{ + id: 10, + cmd: "insert".to_string(), + key: Some("some:key".to_string()), + payload: Some(Payload::JSON("{}".to_string())), + }); + + let m = "1|done".parse::() + .expect("Expected message to parse"); + + assert_eq!(m, Message{ + id: 1, + cmd: "done".to_string(), + key: None, + payload: None + }); + + let m = "".parse::() + .expect_err("Expected parsing to fail"); + if let MessageError::InvalidID = m {} else { + panic!("unexpected error value: {}", m) + } + + let m = "1".parse::() + .expect_err("Expected parsing to fail"); + + if let MessageError::MissingCommand = m {} else { + panic!("unexpected error value: {}", m) + } + } +} diff --git a/desktop/tauri/src-tauri/src/portapi/mod.rs b/desktop/tauri/src-tauri/src/portapi/mod.rs new file mode 100644 index 00000000..67fd2710 --- /dev/null +++ b/desktop/tauri/src-tauri/src/portapi/mod.rs @@ -0,0 +1,4 @@ +pub mod client; +pub mod message; +pub mod types; +pub mod models; \ No newline at end of file diff --git a/desktop/tauri/src-tauri/src/portapi/models/config.rs b/desktop/tauri/src-tauri/src/portapi/models/config.rs new file mode 100644 index 00000000..e29474a8 --- /dev/null +++ b/desktop/tauri/src-tauri/src/portapi/models/config.rs @@ -0,0 +1,18 @@ +use serde::*; +use super::super::message::Payload; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct BooleanValue { + #[serde(rename = "Value")] + pub value: Option, +} + +impl TryInto for BooleanValue { + type Error = serde_json::Error; + + fn try_into(self) -> Result { + let str = serde_json::to_string(&self)?; + + Ok(Payload::JSON(str)) + } +} \ No newline at end of file diff --git a/desktop/tauri/src-tauri/src/portapi/models/mod.rs b/desktop/tauri/src-tauri/src/portapi/models/mod.rs new file mode 100644 index 00000000..91336dd0 --- /dev/null +++ b/desktop/tauri/src-tauri/src/portapi/models/mod.rs @@ -0,0 +1,4 @@ +pub mod config; +pub mod spn; +pub mod notification; +pub mod subsystem; \ No newline at end of file diff --git a/desktop/tauri/src-tauri/src/portapi/models/notification.rs b/desktop/tauri/src-tauri/src/portapi/models/notification.rs new file mode 100644 index 00000000..51f4ece4 --- /dev/null +++ b/desktop/tauri/src-tauri/src/portapi/models/notification.rs @@ -0,0 +1,70 @@ +use serde::*; + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct Notification { + #[serde(rename = "EventID")] + pub event_id: String, + + #[serde(rename = "GUID")] + pub guid: String, + + #[serde(rename = "Type")] + pub notification_type: NotificationType, + + #[serde(rename = "Message")] + pub message: String, + + #[serde(rename = "Title")] + pub title: String, + #[serde(rename = "Category")] + pub category: String, + + #[serde(rename = "EventData")] + pub data: serde_json::Value, + + #[serde(rename = "Expires")] + pub expires: u64, + + #[serde(rename = "State")] + pub state: String, + + #[serde(rename = "AvailableActions")] + pub actions: Vec, + + #[serde(rename = "SelectedActionID")] + pub selected_action_id: String, + + #[serde(rename = "ShowOnSystem")] + pub show_on_system: bool, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct Action { + #[serde(rename = "ID")] + pub id: String, + + #[serde(rename = "Text")] + pub text: String, + + #[serde(rename = "Type")] + pub action_type: String, + + #[serde(rename = "Payload")] + pub payload: serde_json::Value, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct NotificationType(i32); + +#[allow(dead_code)] +pub const INFO: NotificationType = NotificationType(0); + +#[allow(dead_code)] +pub const WARN: NotificationType = NotificationType(1); + +#[allow(dead_code)] +pub const PROMPT: NotificationType = NotificationType(2); + +#[allow(dead_code)] +pub const ERROR: NotificationType = NotificationType(3); + diff --git a/desktop/tauri/src-tauri/src/portapi/models/spn.rs b/desktop/tauri/src-tauri/src/portapi/models/spn.rs new file mode 100644 index 00000000..549c2e27 --- /dev/null +++ b/desktop/tauri/src-tauri/src/portapi/models/spn.rs @@ -0,0 +1,8 @@ +use serde::*; + + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct SPNStatus { + #[serde(rename = "Status")] + pub status: String, +} \ No newline at end of file diff --git a/desktop/tauri/src-tauri/src/portapi/models/subsystem.rs b/desktop/tauri/src-tauri/src/portapi/models/subsystem.rs new file mode 100644 index 00000000..c8b0ea27 --- /dev/null +++ b/desktop/tauri/src-tauri/src/portapi/models/subsystem.rs @@ -0,0 +1,45 @@ +#![allow(dead_code)] +use serde::*; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct ModuleStatus { + #[serde(rename = "Name")] + pub name: String, + + #[serde(rename = "Enabled")] + pub enabled: bool, + + #[serde(rename = "Status")] + pub status: u8, + + #[serde(rename = "FailureStatus")] + pub failure_status: u8, + + #[serde(rename = "FailureID")] + pub failure_id: String, + + #[serde(rename = "FailureMsg")] + pub failure_msg: String, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct Subsystem { + #[serde(rename = "ID")] + pub id: String, + + #[serde(rename = "Name")] + pub name: String, + + #[serde(rename = "Description")] + pub description: String, + + #[serde(rename = "Modules")] + pub module_status: Vec, + + #[serde(rename = "FailureStatus")] + pub failure_status: u8, +} +pub const FAILURE_NONE: u8 = 0; +pub const FAILURE_HINT: u8 = 1; +pub const FAILURE_WARNING: u8 = 2; +pub const FAILURE_ERROR: u8 = 3; diff --git a/desktop/tauri/src-tauri/src/portapi/types.rs b/desktop/tauri/src-tauri/src/portapi/types.rs new file mode 100644 index 00000000..632f24f4 --- /dev/null +++ b/desktop/tauri/src-tauri/src/portapi/types.rs @@ -0,0 +1,199 @@ + +use super::message::*; + +/// Request is a strongly typed request message +/// that can be converted to a `portapi::message::Message` +/// object for further use by the client (`portapi::client::PortAPI`). +#[derive(PartialEq, Debug)] +pub enum Request { + Get(String), + Query(String), + Subscribe(String), + QuerySubscribe(String), + Create(String, Payload), + Update(String, Payload), + Insert(String, Payload), + Delete(String), + Cancel, +} + +/// Implementation to convert a internal `portapi::message::Message` to a valid +/// `Request` variant. +/// +/// Any error returned will be of type `portapi::message::MessageError`. +impl std::convert::TryFrom for Request { + type Error = MessageError; + + fn try_from(value: Message) -> Result { + match value.cmd.as_str() { + "get" => { + let key = value.key.ok_or(MessageError::MissingKey)?; + Ok(Request::Get(key)) + }, + "query" => { + let key = value.key.ok_or(MessageError::MissingKey)?; + Ok(Request::Query(key)) + }, + "sub" => { + let key = value.key.ok_or(MessageError::MissingKey)?; + Ok(Request::Subscribe(key)) + }, + "qsub" => { + let key = value.key.ok_or(MessageError::MissingKey)?; + Ok(Request::QuerySubscribe(key)) + }, + "create" => { + let key = value.key.ok_or(MessageError::MissingKey)?; + let payload = value.payload.ok_or(MessageError::MissingPayload)?; + Ok(Request::Create(key, payload)) + }, + "update" => { + let key = value.key.ok_or(MessageError::MissingKey)?; + let payload = value.payload.ok_or(MessageError::MissingPayload)?; + Ok(Request::Update(key, payload)) + }, + "insert" => { + let key = value.key.ok_or(MessageError::MissingKey)?; + let payload = value.payload.ok_or(MessageError::MissingPayload)?; + Ok(Request::Insert(key, payload)) + }, + "delete" => { + let key = value.key.ok_or(MessageError::MissingKey)?; + Ok(Request::Delete(key)) + }, + "cancel" => { + Ok(Request::Cancel) + }, + cmd => { + Err(MessageError::UnknownCommand(cmd.to_string())) + } + } + } +} + +/// An implementation to try to convert a `Request` variant into a valid +/// `portapi::message::Message` struct. +/// +/// While this implementation does not yet return any errors, it's expected that +/// additional validation will be added in the future so users should already expect +/// to receive `portapi::message::MessageError`s. +impl std::convert::TryFrom for Message { + type Error = MessageError; + + fn try_from(value: Request) -> Result { + match value { + Request::Get(key) => Ok(Message { id: 0, cmd: "get".to_string(), key: Some(key), payload: None }), + Request::Query(key) => Ok(Message { id: 0, cmd: "query".to_string(), key: Some(key), payload: None }), + Request::Subscribe(key) => Ok(Message { id: 0, cmd: "sub".to_string(), key: Some(key), payload: None }), + Request::QuerySubscribe(key) => Ok(Message { id: 0, cmd: "qsub".to_string(), key: Some(key), payload: None }), + Request::Create(key, value) => Ok(Message{ id: 0, cmd: "create".to_string(), key: Some(key), payload: Some(value)}), + Request::Update(key, value) => Ok(Message{ id: 0, cmd: "update".to_string(), key: Some(key), payload: Some(value)}), + Request::Insert(key, value) => Ok(Message{ id: 0, cmd: "insert".to_string(), key: Some(key), payload: Some(value)}), + Request::Delete(key) => Ok(Message { id: 0, cmd: "delete".to_string(), key: Some(key), payload: None }), + Request::Cancel => Ok(Message { id: 0, cmd: "cancel".to_string(), key: None, payload: None }), + } + } +} + + +/// Response is strongly types PortAPI response message. +/// that can be converted to a `portapi::message::Message` +/// object for further use by the client (`portapi::client::PortAPI`). +#[derive(PartialEq, Debug)] +pub enum Response { + Ok(String, Payload), + Update(String, Payload), + New(String, Payload), + Delete(String), + Success, + Error(String), + Warning(String), + Done +} + +/// Implementation to convert a internal `portapi::message::Message` to a valid +/// `Response` variant. +/// +/// Any error returned will be of type `portapi::message::MessageError`. +impl std::convert::TryFrom for Response { + type Error = MessageError; + + fn try_from(value: Message) -> Result { + match value.cmd.as_str() { + "ok" => { + let key = value.key.ok_or(MessageError::MissingKey)?; + let payload = value.payload.ok_or(MessageError::MissingPayload)?; + + Ok(Response::Ok(key, payload)) + }, + "upd" => { + let key = value.key.ok_or(MessageError::MissingKey)?; + let payload = value.payload.ok_or(MessageError::MissingPayload)?; + + Ok(Response::Update(key, payload)) + }, + "new" => { + let key = value.key.ok_or(MessageError::MissingKey)?; + let payload = value.payload.ok_or(MessageError::MissingPayload)?; + + Ok(Response::New(key, payload)) + }, + "del" => { + let key = value.key.ok_or(MessageError::MissingKey)?; + + Ok(Response::Delete(key)) + }, + "success" => { + Ok(Response::Success) + }, + "error" => { + let key = value.key.ok_or(MessageError::MissingKey)?; + + Ok(Response::Error(key)) + }, + "warning" => { + let key = value.key.ok_or(MessageError::MissingKey)?; + + Ok(Response::Warning(key)) + }, + "done" => { + Ok(Response::Done) + }, + cmd => Err(MessageError::UnknownCommand(cmd.to_string())) + } + } +} + +/// An implementation to try to convert a `Response` variant into a valid +/// `portapi::message::Message` struct. +/// +/// While this implementation does not yet return any errors, it's expected that +/// additional validation will be added in the future so users should already expect +/// to receive `portapi::message::MessageError`s. +impl std::convert::TryFrom for Message { + type Error = MessageError; + + fn try_from(value: Response) -> Result { + match value { + Response::Ok(key, payload) => Ok(Message{id: 0, cmd: "ok".to_string(), key: Some(key), payload: Some(payload)}), + Response::Update(key, payload) => Ok(Message{id: 0, cmd: "upd".to_string(), key: Some(key), payload: Some(payload)}), + Response::New(key, payload) => Ok(Message{id: 0, cmd: "new".to_string(), key: Some(key), payload: Some(payload)}), + Response::Delete(key ) => Ok(Message{id: 0, cmd: "del".to_string(), key: Some(key), payload: None}), + Response::Success => Ok(Message{id: 0, cmd: "success".to_string(), key: None, payload: None}), + Response::Warning(key) => Ok(Message{id: 0, cmd: "warning".to_string(), key: Some(key), payload: None}), + Response::Error(key) => Ok(Message{id: 0, cmd: "error".to_string(), key: Some(key), payload: None}), + Response::Done => Ok(Message{id: 0, cmd: "done".to_string(), key: None, payload: None}), + } + } +} + + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub struct Record { + pub created: u64, + pub deleted: u64, + pub expires: u64, + pub modified: u64, + pub key: String, +} \ No newline at end of file diff --git a/desktop/tauri/src-tauri/src/portmaster/commands.rs b/desktop/tauri/src-tauri/src/portmaster/commands.rs new file mode 100644 index 00000000..dfa2b222 --- /dev/null +++ b/desktop/tauri/src-tauri/src/portmaster/commands.rs @@ -0,0 +1,182 @@ +use super::PortmasterPlugin; +use crate::service::get_service_manager; +use crate::service::ServiceManager; +use log::debug; +use std::sync::atomic::Ordering; +use tauri::{Manager, Runtime, State, Window}; + +pub type Result = std::result::Result; + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct Error { + pub error: String, +} + +#[tauri::command] +pub fn should_show( + _window: Window, + portmaster: State<'_, PortmasterPlugin>, +) -> Result { + if portmaster.get_show_after_bootstrap() { + debug!("[tauri:rpc:should_show] application should show after bootstrap"); + + Ok("show".to_string()) + } else { + debug!("[tauri:rpc:should_show] application should hide after bootstrap"); + + Ok("hide".to_string()) + } +} + +#[tauri::command] +pub fn should_handle_prompts( + _window: Window, + portmaster: State<'_, PortmasterPlugin>, +) -> Result { + if portmaster.handle_prompts.load(Ordering::Relaxed) { + Ok("true".to_string()) + } else { + Ok("false".to_string()) + } +} + +#[tauri::command] +pub fn get_state( + _window: Window, + portmaster: State<'_, PortmasterPlugin>, + key: String, +) -> Result { + let value = portmaster.get_state(key); + + if let Some(value) = value { + Ok(value) + } else { + Ok("".to_string()) + } +} + +#[tauri::command] +pub fn set_state( + _window: Window, + portmaster: State<'_, PortmasterPlugin>, + key: String, + value: String, +) -> Result { + portmaster.set_state(key, value); + + Ok("".to_string()) +} + +#[cfg(target_os = "linux")] +#[tauri::command] +pub fn get_app_info( + window: Window, + response_id: String, + matching_path: String, + exec_path: String, + pid: i64, + cmdline: String, +) -> Result { + let mut id = response_id; + + let info = crate::xdg::ProcessInfo { + cmdline, + exec_path, + pid, + matching_path, + }; + + if id == "" { + id = uuid::Uuid::new_v4().to_string() + } + let cloned = id.clone(); + + std::thread::spawn(move || match crate::xdg::get_app_info(info) { + Ok(info) => window.emit(&id, info), + Err(err) => window.emit( + &id, + Error { + error: err.to_string(), + }, + ), + }); + + Ok(cloned) +} + +#[cfg(target_os = "windows")] +#[tauri::command] +pub fn get_app_info( + window: Window, + response_id: String, + _matching_path: String, + _exec_path: String, + _pid: i64, + _cmdline: String, +) -> Result { + let mut id = response_id; + + if id == "" { + id = uuid::Uuid::new_v4().to_string() + } + let cloned = id.clone(); + + std::thread::spawn(move || { + let _ = window.emit( + &id, + Error { + error: "Unsupported OS".to_string(), + }, + ); + }); + + Ok(cloned) +} + +#[tauri::command] +pub fn get_service_manager_status(window: Window, response_id: String) -> Result { + let mut id = response_id; + + if id == "" { + id = uuid::Uuid::new_v4().to_string(); + } + let cloned = id.clone(); + + std::thread::spawn(move || { + let result = match get_service_manager() { + Ok(sm) => sm.status().map_err(|err| err.to_string()), + Err(err) => Err(err.to_string()), + }; + + match result { + Ok(result) => window.emit(&id, &result), + Err(err) => window.emit(&id, Error { error: err }), + } + }); + + Ok(cloned) +} + +#[tauri::command] +pub fn start_service(window: Window, response_id: String) -> Result { + let mut id = response_id; + + if id == "" { + id = uuid::Uuid::new_v4().to_string(); + } + let cloned = id.clone(); + + std::thread::spawn(move || { + let result = match get_service_manager() { + Ok(sm) => sm.start().map_err(|err| err.to_string()), + Err(err) => Err(err.to_string()), + }; + + match result { + Ok(result) => window.emit(&id, &result), + Err(err) => window.emit(&id, Error { error: err }), + } + }); + + Ok(cloned) +} diff --git a/desktop/tauri/src-tauri/src/portmaster/mod.rs b/desktop/tauri/src-tauri/src/portmaster/mod.rs new file mode 100644 index 00000000..a5844949 --- /dev/null +++ b/desktop/tauri/src-tauri/src/portmaster/mod.rs @@ -0,0 +1,294 @@ +/// This module contains a custom tauri plugin that handles all communication +/// with the angular app loaded from the portmaster api. +/// +/// Using a custom-plugin for this has the advantage that all code that has +/// access to a tauri::Window or a tauri::AppHandle can get access to the +/// portmaster plugin using the Runtime/Manager extension by just calling +/// window.portmaster() or app_handle.portmaster(). +/// +/// Any portmaster related features (like changing a portmaster setting) should +/// live in this module. +/// +/// Code that handles windows should NOT live here but should rather be placed +/// in the crate root. +// The commands module contains tauri commands that are available to Javascript +// using the invoke() and our custom invokeAsync() command. +mod commands; + +// The websocket module spawns an async function on tauri's runtime that manages +// a persistent connection to the Portmaster websocket API and updates the tauri Portmaster +// Plugin instance. +mod websocket; + +// The notification module manages system notifications from portmaster. +mod notifications; + +use crate::portapi::{ + client::PortAPI, message::Payload, models::config::BooleanValue, types::Request, +}; +use std::{ + collections::HashMap, + sync::atomic::{AtomicBool, Ordering}, +}; + +use log::debug; +use serde; +use std::sync::Mutex; +use tauri::{ + plugin::{Builder, TauriPlugin}, + AppHandle, Manager, Runtime, +}; + +pub trait Handler { + fn on_connect(&mut self, cli: PortAPI) -> (); + fn on_disconnect(&mut self); +} + +pub struct PortmasterPlugin { + #[allow(dead_code)] + app: AppHandle, + + // state allows the angular application to store arbitrary values in the + // tauri application memory using the get_state and set_state + // tauri::commands. + state: Mutex>, + + // an atomic boolean that indicates if we're currently connected to + // portmaster or not. + is_reachable: AtomicBool, + + // holds the portapi client if any. + api: Mutex>, + + // a vector of handlers that should be invoked on connect and disconnect of + // the portmaster API. + handlers: Mutex>>, + + // whether or not we should handle notifications here. + handle_notifications: AtomicBool, + + // whether or not we should handle prompts. + handle_prompts: AtomicBool, + + // whether or not the angular application should call window.show after it + // finished bootstrapping. + should_show_after_bootstrap: AtomicBool, +} + +impl PortmasterPlugin { + /// Returns a state stored in the portmaster plugin. + pub fn get_state(&self, key: String) -> Option { + let map = self.state.lock(); + + if let Ok(map) = map { + match map.get(&key) { + Some(value) => Some(value.clone()), + None => None, + } + } else { + None + } + } + + /// Adds a new state to the portmaster plugin. + pub fn set_state(&self, key: String, value: String) { + let map = self.state.lock(); + + if let Ok(mut map) = map { + map.insert(key, value); + } + } + + /// Reports wheter or not we're currently connected to the Portmaster API. + pub fn is_reachable(&self) -> bool { + self.is_reachable.load(Ordering::Relaxed) + } + + /// Registers a new connection handler that is called on connect + /// and disconnect of the Portmaster websocket API. + pub fn register_handler(&self, mut handler: impl Handler + Send + 'static) { + // register_handler can only be invoked after the plugin setup + // completed. in this case, the websocket thread is already spawned and + // we might already be connected or know that the connection failed. + // Call the respective handler method immediately now. + if let Some(api) = self.get_api() { + handler.on_connect(api); + } else { + handler.on_disconnect(); + } + + if let Ok(mut handlers) = self.handlers.lock() { + handlers.push(Box::new(handler)); + } + } + + /// Returns the current portapi client. + pub fn get_api(&self) -> Option { + if let Ok(mut api) = self.api.lock() { + match &mut *api { + Some(api) => Some(api.clone()), + None => None, + } + } else { + None + } + } + + /// Feature functions (enable/disable certain features). + + /// Configures whether or not our tauri app should show system + /// notifications. This excludes connection prompts. Use + /// with_connection_prompts to enable handling of connection prompts. + pub fn with_notification_support(&self, enable: bool) { + self.handle_notifications.store(enable, Ordering::Relaxed); + + // kick of the notification handler if we are connected. + if enable { + self.start_notification_handler(); + } + } + + /// Configures whether or not our angular application should show connection + /// prompts via tauri. + pub fn with_connection_prompts(&self, enable: bool) { + self.handle_prompts.store(enable, Ordering::Relaxed); + } + + /// Whether or not the angular application should call window.show after it + /// finished bootstrapping. + pub fn set_show_after_bootstrap(&self, show: bool) { + self.should_show_after_bootstrap + .store(show, Ordering::Relaxed); + } + + /// Returns whether or not the angular application should call window.show + /// after it finished bootstrapping. + pub fn get_show_after_bootstrap(&self) -> bool { + self.should_show_after_bootstrap.load(Ordering::Relaxed) + } + + /// Tells the angular applicatoin to show the window by emitting an event. + /// It calls set_show_after_bootstrap(true) automatically so the application + /// also shows after bootstrapping. + pub fn show_window(&self) { + debug!("[tauri] showing main window"); + + // set show_after_bootstrap to true so the app will even show if it + // misses the event below because it's still bootstrapping. + self.set_show_after_bootstrap(true); + + // ignore the error here, there's nothing we could do about it anyways. + let _ = self.app.emit("portmaster:show", ""); + } + + /// Enables or disables the SPN. + pub fn set_spn_enabled(&self, enabled: bool) { + if let Some(api) = self.get_api() { + let body: Result = BooleanValue { + value: Some(enabled), + } + .try_into(); + + if let Ok(payload) = body { + tauri::async_runtime::spawn(async move { + _ = api + .request(Request::Update("config:spn/enable".to_string(), payload)) + .await; + }); + } + } + } + + //// Internal functions + fn start_notification_handler(&self) { + if let Some(api) = self.get_api() { + let cli = api.clone(); + tauri::async_runtime::spawn(async move { + notifications::notification_handler(cli).await; + }); + } + } + + /// Internal method to call all on_connect handlers + fn on_connect(&self, api: PortAPI) { + self.is_reachable.store(true, Ordering::Relaxed); + + // store the new api client. + let mut guard = self.api.lock().unwrap(); + *guard = Some(api.clone()); + drop(guard); + + // fire-off the notification handler. + if self.handle_notifications.load(Ordering::Relaxed) { + self.start_notification_handler(); + } + + if let Ok(mut handlers) = self.handlers.lock() { + for handler in handlers.iter_mut() { + handler.on_connect(api.clone()); + } + } + } + + /// Internal method to call all on_disconnect handlers + fn on_disconnect(&self) { + self.is_reachable.store(false, Ordering::Relaxed); + + // clear the current api client reference. + let mut guard = self.api.lock().unwrap(); + *guard = None; + drop(guard); + + if let Ok(mut handlers) = self.handlers.lock() { + for handler in handlers.iter_mut() { + handler.on_disconnect(); + } + } + } +} + +pub trait PortmasterExt { + fn portmaster(&self) -> &PortmasterPlugin; +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct Config {} + +impl> PortmasterExt for T { + fn portmaster(&self) -> &PortmasterPlugin { + self.state::>().inner() + } +} + +pub fn init() -> TauriPlugin> { + Builder::>::new("portmaster") + .invoke_handler(tauri::generate_handler![ + commands::get_app_info, + commands::get_service_manager_status, + commands::start_service, + commands::get_state, + commands::set_state, + commands::should_show, + commands::should_handle_prompts + ]) + .setup(|app, _api| { + let plugin = PortmasterPlugin { + app: app.clone(), + state: Mutex::new(HashMap::new()), + is_reachable: AtomicBool::new(false), + handlers: Mutex::new(Vec::new()), + api: Mutex::new(None), + handle_notifications: AtomicBool::new(false), + handle_prompts: AtomicBool::new(false), + should_show_after_bootstrap: AtomicBool::new(true), + }; + + app.manage(plugin); + + // fire of the websocket handler + websocket::start_websocket_thread(app.clone()); + + Ok(()) + }) + .build() +} diff --git a/desktop/tauri/src-tauri/src/portmaster/notifications.rs b/desktop/tauri/src-tauri/src/portmaster/notifications.rs new file mode 100644 index 00000000..88472639 --- /dev/null +++ b/desktop/tauri/src-tauri/src/portmaster/notifications.rs @@ -0,0 +1,103 @@ +use crate::portapi::client::*; +use crate::portapi::message::*; +use crate::portapi::models::notification::*; +use crate::portapi::types::*; +use log::error; +use notify_rust; +use serde_json::json; +#[allow(unused_imports)] +use tauri::async_runtime; + +pub async fn notification_handler(cli: PortAPI) { + let res = cli + .request(Request::QuerySubscribe("query notifications:".to_string())) + .await; + + if let Ok(mut rx) = res { + while let Some(msg) = rx.recv().await { + let res = match msg { + Response::Ok(key, payload) => Some((key, payload)), + Response::New(key, payload) => Some((key, payload)), + Response::Update(key, payload) => Some((key, payload)), + _ => None, + }; + + if let Some((key, payload)) = res { + match payload.parse::() { + Ok(n) => { + // Skip if this one should not be shown using the system notifications + if !n.show_on_system { + return; + } + + // Skip if this action has already been acted on + if n.selected_action_id != "" { + return; + } + + // TODO(ppacher): keep a reference of open notifications and close them + // if the user reacted inside the UI: + + let mut notif = notify_rust::Notification::new(); + notif.body(&n.message); + notif.timeout(notify_rust::Timeout::Never); // TODO(ppacher): use n.expires to calculate the timeout. + notif.summary(&n.title); + notif.icon("portmaster"); + + for action in n.actions { + notif.action(&action.id, &action.text); + } + + #[cfg(target_os = "linux")] + { + let cli_clone = cli.clone(); + async_runtime::spawn(async move { + let res = notif.show(); + match res { + Ok(handle) => { + handle.wait_for_action(|action| { + match action { + "__closed" => { + // timeout + } + + value => { + let value = value.to_string().clone(); + + async_runtime::spawn(async move { + let _ = cli_clone + .request(Request::Update( + key, + Payload::JSON( + json!({ + "SelectedActionID": value + }) + .to_string(), + ), + )) + .await; + }); + } + } + }) + } + Err(err) => { + error!("failed to display notification: {}", err); + } + } + }); + } + } + Err(err) => match err { + ParseError::JSON(err) => { + error!("failed to parse notification: {}", err); + } + _ => { + error!("unknown error when parsing notifications payload"); + } + }, + } + } + } + } +} diff --git a/desktop/tauri/src-tauri/src/portmaster/websocket.rs b/desktop/tauri/src-tauri/src/portmaster/websocket.rs new file mode 100644 index 00000000..6dca8519 --- /dev/null +++ b/desktop/tauri/src-tauri/src/portmaster/websocket.rs @@ -0,0 +1,45 @@ +use super::PortmasterExt; +use crate::portapi::client::connect; +use log::{debug, error, info, warn}; +use tauri::{AppHandle, Runtime}; +use tokio::time::{sleep, Duration}; + +/// Starts a backround thread (via tauri::async_runtime) that connects to the Portmaster +/// Websocket database API. +pub fn start_websocket_thread(app: AppHandle) { + let app = app.clone(); + + tauri::async_runtime::spawn(async move { + loop { + debug!("Trying to connect to websocket endpoint"); + + let api = connect("ws://127.0.0.1:817/api/database/v1").await; + + match api { + Ok(cli) => { + let portmaster = app.portmaster(); + + info!("Successfully connected to portmaster"); + + portmaster.on_connect(cli.clone()); + + while !cli.is_closed() { + let _ = sleep(Duration::from_secs(1)).await; + } + + portmaster.on_disconnect(); + + warn!("lost connection to portmaster, retrying ....") + } + Err(err) => { + error!("failed to create portapi client: {}", err); + + app.portmaster().on_disconnect(); + + // sleep and retry + sleep(Duration::from_secs(2)).await; + } + } + } + }); +} diff --git a/desktop/tauri/src-tauri/src/service/manager.rs b/desktop/tauri/src-tauri/src/service/manager.rs new file mode 100644 index 00000000..7a79e061 --- /dev/null +++ b/desktop/tauri/src-tauri/src/service/manager.rs @@ -0,0 +1,17 @@ +use std::process::{Command, ExitStatus, Stdio}; +use std::{fs, io}; + +use thiserror::Error; + +#[cfg(target_os = "linux")] +use std::os::unix::fs::PermissionsExt; + +use super::status::StatusResult; + +static SYSTEMCTL: &str = "systemctl"; +// TODO(ppacher): add support for kdesudo and gksudo + +enum SudoCommand { + Pkexec, + Gksu, +} diff --git a/desktop/tauri/src-tauri/src/service/mod.rs b/desktop/tauri/src-tauri/src/service/mod.rs new file mode 100644 index 00000000..ac55a39f --- /dev/null +++ b/desktop/tauri/src-tauri/src/service/mod.rs @@ -0,0 +1,76 @@ +// pub mod manager; +pub mod status; + +#[cfg(target_os = "linux")] +mod systemd; + +#[cfg(target_os = "windows")] +mod windows_service; + +use std::process::ExitStatus; + +#[cfg(target_os = "linux")] +use crate::service::systemd::SystemdServiceManager; + +use log::info; +use thiserror::Error; + +use self::status::StatusResult; + +#[allow(dead_code)] +#[derive(Error, Debug)] +pub enum ServiceManagerError { + #[error("unsupported service manager")] + UnsupportedServiceManager, + + #[error("unsupported operating system")] + UnsupportedOperatingSystem, + + #[error(transparent)] + FromUtf8Error(#[from] std::string::FromUtf8Error), + + #[error(transparent)] + IoError(#[from] std::io::Error), + + #[error("{0} output={1}")] + Other(ExitStatus, String), + + #[error("{0}")] + WindowsError(String), +} + +pub type Result = std::result::Result; + +/// A common interface to the system manager service (might be systemd, openrc, sc.exe, ...) +pub trait ServiceManager { + fn status(&self) -> Result; + fn start(&self) -> Result; +} + +struct EmptyServiceManager(); + +impl ServiceManager for EmptyServiceManager { + fn status(&self) -> Result { + Err(ServiceManagerError::UnsupportedServiceManager) + } + + fn start(&self) -> Result { + Err(ServiceManagerError::UnsupportedServiceManager) + } +} + +pub fn get_service_manager() -> Result { + #[cfg(target_os = "linux")] + { + if SystemdServiceManager::is_installed() { + info!("system service manager: systemd"); + + Ok(SystemdServiceManager {}) + } else { + Err(ServiceManagerError::UnsupportedServiceManager) + } + } + + #[cfg(target_os = "windows")] + return Ok(windows_service::SERVICE_MANGER.clone()); +} diff --git a/desktop/tauri/src-tauri/src/service/status.rs b/desktop/tauri/src-tauri/src/service/status.rs new file mode 100644 index 00000000..30f4841d --- /dev/null +++ b/desktop/tauri/src-tauri/src/service/status.rs @@ -0,0 +1,27 @@ +use serde::{Serialize, Deserialize}; + +/// SystemResult defines the "success" codes when querying or starting +/// a system service. +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub enum StatusResult { + // The requested system service is installed and currently running. + Running, + + // The requested system service is installed but currently stopped. + Stopped, + + // NotFound is returned when the system service (systemd unit for linux) + // has not been found and the system and likely means the Portmaster installtion + // is broken all together. + NotFound, +} + +impl std::fmt::Display for StatusResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StatusResult::Running => write!(f, "running"), + StatusResult::Stopped => write!(f, "stopped"), + StatusResult::NotFound => write!(f, "not installed") + } + } +} diff --git a/desktop/tauri/src-tauri/src/service/systemd.rs b/desktop/tauri/src-tauri/src/service/systemd.rs new file mode 100644 index 00000000..770a9139 --- /dev/null +++ b/desktop/tauri/src-tauri/src/service/systemd.rs @@ -0,0 +1,246 @@ +use log::{debug, error}; + +use super::status::StatusResult; +use super::{Result, ServiceManager, ServiceManagerError}; +use std::os::unix::fs::PermissionsExt; +use std::{ + fs, io, + process::{Command, ExitStatus, Stdio}, +}; + +static SYSTEMCTL: &str = "systemctl"; +// TODO(ppacher): add support for kdesudo and gksudo + +enum SudoCommand { + Pkexec, + Gksu, +} + +impl From for ServiceManagerError { + fn from(output: std::process::Output) -> Self { + let msg = String::from_utf8(output.stderr) + .ok() + .filter(|s| !s.trim().is_empty()) + .or_else(|| { + String::from_utf8(output.stdout) + .ok() + .filter(|s| !s.trim().is_empty()) + }) + .unwrap_or_else(|| format!("Failed to run `systemctl`")); + + ServiceManagerError::Other(output.status, msg) + } +} + +/// System Service manager implementation for Linux based distros. +pub struct SystemdServiceManager {} + +impl SystemdServiceManager { + /// Checks if systemctl is available in /sbin/ /bin, /usr/bin or /usr/sbin. + /// + /// Note that we explicitly check those paths to avoid returning true in case + /// there's a systemctl binary in the cwd and PATH includes . since this may + /// pose a security risk of running an untrusted binary with root privileges. + pub fn is_installed() -> bool { + let paths = vec![ + "/sbin/systemctl", + "/bin/systemctl", + "/usr/sbin/systemctl", + "/usr/bin/systemctl", + ]; + + for path in paths { + debug!("checking for systemctl at path {}", path); + + match fs::metadata(path) { + Ok(md) => { + debug!("found systemctl at path {} ", path); + + if md.is_file() && md.permissions().mode() & 0o111 != 0 { + return true; + } + + error!( + "systemctl binary found but invalid permissions: {}", + md.permissions().mode().to_string() + ); + } + Err(err) => { + error!( + "failed to check systemctl binary at {}: {}", + path, + err.to_string() + ); + + continue; + } + }; + } + + error!("failed to find systemctl binary"); + + false + } +} + +impl ServiceManager for SystemdServiceManager { + fn status(&self) -> super::Result { + let name = "portmaster.service"; + let result = systemctl("is-active", name, false); + + match result { + // If `systemctl is-active` returns without an error code and stdout matches "active" (just to guard againt + // unhandled cases), the service can be considered running. + Ok(stdout) => { + let mut copy = stdout.to_owned(); + trim_newline(&mut copy); + + if copy != "active" { + // make sure the output is as we expected + Err(ServiceManagerError::Other(ExitStatus::default(), stdout)) + } else { + Ok(StatusResult::Running) + } + } + + Err(e) => { + if let ServiceManagerError::Other(_err, ref output) = e { + let mut copy = output.to_owned(); + trim_newline(&mut copy); + + if copy == "inactive" { + return Ok(StatusResult::Stopped); + } + } else { + error!("failed to run 'systemctl is-active': {}", e.to_string()); + } + + // Failed to check if the unit is running + match systemctl("cat", name, false) { + // "systemctl cat" seems to no have stable exit codes so we need + // to check the output if it looks like "No files found for yyyy.service" + // At least, the exit code are not documented for systemd v255 (newest at the time of writing) + Err(ServiceManagerError::Other(status, msg)) => { + if msg.contains("No files found for") { + Ok(StatusResult::NotFound) + } else { + Err(ServiceManagerError::Other(status, msg)) + } + } + + // Any other error type means something went completely wrong while running systemctl altogether. + Err(e) => Err(e), + + // Fine, systemctl cat worked so if the output is "inactive" we know the service is installed + // but stopped. + Ok(_) => { + // Unit seems to be installed so check the output of result + let mut stderr = e.to_string(); + trim_newline(&mut stderr); + + if stderr == "inactive" { + Ok(StatusResult::Stopped) + } else { + Err(e) + } + } + } + } + } + } + + fn start(&self) -> Result { + let name = "portmaster.service"; + + // This time we need to run as root through pkexec or similar binaries like kdesudo/gksudo. + systemctl("start", name, true)?; + + // Check the status again to be sure it's started now + self.status() + } +} + +fn systemctl( + cmd: &str, + unit: &str, + run_as_root: bool, +) -> std::result::Result { + let output = run(run_as_root, SYSTEMCTL, vec![cmd, unit])?; + + // The command have been able to run (i.e. has been spawned and executed by the kernel). + // We now need to check the exit code and "stdout/stderr" output in case of an error. + if output.status.success() { + Ok(String::from_utf8(output.stdout)?) + } else { + Err(output.into()) + } +} + +fn run<'a>(root: bool, cmd: &'a str, args: Vec<&'a str>) -> std::io::Result { + // clone the args vector so we can insert the actual command in case we're running + // through pkexec or friends. This is just callled a couple of times on start-up + // so cloning the vector does not add any mentionable performance impact here and it's better + // than expecting a mutalble vector in the first place. + + let mut args = args.to_vec(); + + let mut command = match root { + true => { + // if we run through pkexec and friends we need to append cmd as the second argument. + + args.insert(0, cmd); + match get_sudo_cmd() { + Ok(cmd) => { + match cmd { + SudoCommand::Pkexec => { + // disable the internal text-based prompt agent from pkexec because it won't work anyway. + args.insert(0, "--disable-internal-agent"); + Command::new("/usr/bin/pkexec") + } + SudoCommand::Gksu => { + args.insert(0, "--message=Please enter your password:"); + args.insert(1, "--sudo-mode"); + + Command::new("/usr/bin/gksudo") + } + } + } + Err(err) => return Err(err), + } + } + false => Command::new(cmd), + }; + + command.env("LC_ALL", "C"); + + command + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + command.args(args).output() +} + +fn trim_newline(s: &mut String) { + if s.ends_with('\n') { + s.pop(); + if s.ends_with('\r') { + s.pop(); + } + } +} + +fn get_sudo_cmd() -> std::result::Result { + if let Ok(_) = fs::metadata("/usr/bin/pkexec") { + return Ok(SudoCommand::Pkexec); + } + + if let Ok(_) = fs::metadata("/usr/bin/gksudo") { + return Ok(SudoCommand::Gksu); + } + + Err(std::io::Error::new( + io::ErrorKind::NotFound, + "failed to detect sudo command", + )) +} diff --git a/desktop/tauri/src-tauri/src/service/windows_service.rs b/desktop/tauri/src-tauri/src/service/windows_service.rs new file mode 100644 index 00000000..2146cc41 --- /dev/null +++ b/desktop/tauri/src-tauri/src/service/windows_service.rs @@ -0,0 +1,167 @@ +use std::{ + sync::{Arc, Mutex}, + time::Duration, +}; + +use windows::{ + core::{HSTRING, PCWSTR}, + Win32::{Foundation::HWND, UI::WindowsAndMessaging::SHOW_WINDOW_CMD}, +}; +use windows_service::{ + service::{Service, ServiceAccess}, + service_manager::{ServiceManager, ServiceManagerAccess}, +}; + +const SERVICE_NAME: &str = "PortmasterCore"; + +pub struct WindowsServiceManager { + manager: Option, + service: Option, +} + +lazy_static! { + pub static ref SERVICE_MANGER: Arc> = + Arc::new(Mutex::new(WindowsServiceManager::new())); +} + +impl WindowsServiceManager { + pub fn new() -> Self { + Self { + manager: None, + service: None, + } + } + + fn init_manager(&mut self) -> super::Result<()> { + // Initialize service manager. This connects to the active service database and can query status. + let manager = match ServiceManager::local_computer( + None::<&str>, + ServiceManagerAccess::ENUMERATE_SERVICE, // Only query status is allowed form non privileged application. + ) { + Ok(manager) => manager, + Err(err) => { + return Err(windows_to_manager_err(err)); + } + }; + self.manager = Some(manager); + Ok(()) + } + + fn open_service(&mut self) -> super::Result { + if let None = self.manager { + self.init_manager()?; + } + + if let Some(manager) = &self.manager { + let service = match manager.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS) { + Ok(service) => service, + Err(_) => { + return Ok(false); // Service is not installed. + } + }; + // Service is installed and the state can be queried. + self.service = Some(service); + return Ok(true); + } + + return Err(super::ServiceManagerError::WindowsError( + "failed to initialize manager".to_string(), + )); + } +} + +impl super::ServiceManager for Arc> { + fn status(&self) -> super::Result { + if let Ok(mut manager) = self.lock() { + if let None = manager.service { + // Try to open service + if !manager.open_service()? { + // Service is not installed. + return Ok(super::status::StatusResult::NotFound); + } + } + + if let Some(service) = &manager.service { + match service.query_status() { + Ok(status) => match status.current_state { + windows_service::service::ServiceState::Stopped + | windows_service::service::ServiceState::StopPending + | windows_service::service::ServiceState::PausePending + | windows_service::service::ServiceState::StartPending + | windows_service::service::ServiceState::ContinuePending + | windows_service::service::ServiceState::Paused => { + // Stopped or in a transition state. + return Ok(super::status::StatusResult::Stopped); + } + windows_service::service::ServiceState::Running => { + // Everything expect Running state is considered stopped. + return Ok(super::status::StatusResult::Running); + } + }, + Err(err) => { + return Err(super::ServiceManagerError::WindowsError(err.to_string())); + } + } + } + } + // This should be unreachable. + Ok(super::status::StatusResult::NotFound) + } + + fn start(&self) -> super::Result { + if let Ok(mut service_manager) = self.lock() { + // Check if service is installed. + if let None = &service_manager.service { + if let Err(_) = service_manager.open_service() { + return Ok(super::status::StatusResult::NotFound); + } + } + + // Run service manager with elevated privileges. This will show access popup. + unsafe { + windows::Win32::UI::Shell::ShellExecuteW( + HWND::default(), + &HSTRING::from("runas"), + &HSTRING::from("C:\\Windows\\System32\\sc.exe"), + &HSTRING::from(format!("start {}", SERVICE_NAME)), + PCWSTR::null(), + SHOW_WINDOW_CMD(0), + ); + } + + // Wait for service to start. Timeout 10s (100 * 100ms). + if let Some(service) = &service_manager.service { + for _ in 0..100 { + match service.query_status() { + Ok(status) => { + if let windows_service::service::ServiceState::Running = + status.current_state + { + return Ok(super::status::StatusResult::Running); + } else { + std::thread::sleep(Duration::from_millis(100)); + } + } + Err(err) => return Err(windows_to_manager_err(err)), + } + } + } + // Timeout starting the service. + return Ok(super::status::StatusResult::Stopped); + } + return Err(super::ServiceManagerError::WindowsError( + "failed to start service".to_string(), + )); + } +} + +fn windows_to_manager_err(err: windows_service::Error) -> super::ServiceManagerError { + if let windows_service::Error::Winapi(_) = err { + // Winapi does not contain the full error. Get the actual error from windows. + return super::ServiceManagerError::WindowsError( + windows::core::Error::from_win32().to_string(), // Internally will call `GetLastError()` and parse the result. + ); + } else { + return super::ServiceManagerError::WindowsError(err.to_string()); + } +} diff --git a/desktop/tauri/src-tauri/src/traymenu.rs b/desktop/tauri/src-tauri/src/traymenu.rs new file mode 100644 index 00000000..6456797f --- /dev/null +++ b/desktop/tauri/src-tauri/src/traymenu.rs @@ -0,0 +1,344 @@ +use std::collections::HashMap; +use std::sync::Mutex; + +use log::{debug, error}; +use tauri::{ + menu::{ + CheckMenuItem, CheckMenuItemBuilder, MenuBuilder, MenuItemBuilder, PredefinedMenuItem, + SubmenuBuilder, + }, + tray::{ClickType, TrayIcon, TrayIconBuilder}, + Icon, Manager, Wry, +}; +use tauri_plugin_dialog::DialogExt; + +use crate::{ + portapi::{ + client::PortAPI, + message::ParseError, + models::{ + config::BooleanValue, + spn::SPNStatus, + subsystem::{self, Subsystem}, + }, + types::{Request, Response}, + }, + portmaster::PortmasterExt, + window::{create_main_window, may_navigate_to_ui, open_window}, +}; + +pub type AppIcon = TrayIcon; + +lazy_static! { + // Set once setup_tray_menu executed. + static ref SPN_BUTTON: Mutex>> = Mutex::new(None); +} + +// Icons +// +const BLUE_ICON: &'static [u8] = + include_bytes!("../../assets/icons/pm_light_blue_512.ico"); +const RED_ICON: &'static [u8] = + include_bytes!("../../assets/icons/pm_light_red_512.ico"); +const YELLOW_ICON: &'static [u8] = + include_bytes!("../../assets/icons/pm_light_yellow_512.ico"); +const GREEN_ICON: &'static [u8] = + include_bytes!("../../assets/icons/pm_light_green_512.ico"); + +pub fn setup_tray_menu( + app: &mut tauri::App, +) -> core::result::Result> { + // Tray menu + let close_btn = MenuItemBuilder::with_id("close", "Exit").build(app); + let open_btn = MenuItemBuilder::with_id("open", "Open").build(app); + + let spn = CheckMenuItemBuilder::with_id("spn", "Use SPN").build(app); + + // Store the SPN button reference + let mut button_ref = SPN_BUTTON.lock().unwrap(); + *button_ref = Some(spn.clone()); + + let force_show_window = MenuItemBuilder::with_id("force-show", "Force Show UI").build(app); + let reload_btn = MenuItemBuilder::with_id("reload", "Reload User Interface").build(app); + let developer_menu = SubmenuBuilder::new(app, "Developer") + .items(&[&reload_btn, &force_show_window]) + .build()?; + + // Drop the reference now so we unlock immediately. + drop(button_ref); + + let menu = MenuBuilder::new(app) + .items(&[ + &spn, + &PredefinedMenuItem::separator(app), + &open_btn, + &close_btn, + &developer_menu, + ]) + .build()?; + + let icon = TrayIconBuilder::new() + .icon(Icon::Raw(RED_ICON.to_vec())) + .menu(&menu) + .on_menu_event(move |app, event| match event.id().as_ref() { + "close" => { + let handle = app.clone(); + app.dialog() + .message("This does not stop the Portmaster system service") + .title("Do you really want to quit the user interface?") + .ok_button_label("Yes, exit") + .cancel_button_label("No") + .show(move |answer| { + if answer { + let _ = handle.emit("exit-requested", ""); + handle.exit(0); + } + }); + } + "open" => { + let _ = open_window(app); + } + "reload" => { + if let Ok(mut win) = open_window(app) { + may_navigate_to_ui(&mut win, true); + } + } + "force-show" => { + match create_main_window(app) { + Ok(mut win) => { + may_navigate_to_ui(&mut win, true); + if let Err(err) = win.show() { + error!("[tauri] failed to show window: {}", err.to_string()); + }; + } + Err(err) => { + error!("[tauri] failed to create main window: {}", err.to_string()); + } + }; + } + "spn" => { + let btn = SPN_BUTTON.lock().unwrap(); + + if let Some(bt) = &*btn { + if let Ok(is_checked) = bt.is_checked() { + app.portmaster().set_spn_enabled(is_checked); + } + } + } + other => { + error!("unknown menu event id: {}", other); + } + }) + .on_tray_icon_event(|tray, event| { + // not supported on linux + if event.click_type == ClickType::Left { + let _ = open_window(tray.app_handle()); + } + }) + .build(app)?; + + Ok(icon) +} + +pub fn update_icon(icon: AppIcon, subsystems: HashMap, spn_status: String) { + // iterate over the subsytems and check if there's a module failure + let failure = subsystems + .values() + .into_iter() + .map(|s| s.failure_status) + .fold( + subsystem::FAILURE_NONE, + |acc, s| { + if s > acc { + s + } else { + acc + } + }, + ); + + let next_icon = match failure { + subsystem::FAILURE_WARNING => YELLOW_ICON, + subsystem::FAILURE_ERROR => RED_ICON, + _ => match spn_status.as_str() { + "connected" | "connecting" => BLUE_ICON, + _ => GREEN_ICON, + }, + }; + + _ = icon.set_icon(Some(Icon::Raw(next_icon.to_vec()))); +} + +pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { + let icon = match app.tray() { + Some(icon) => icon, + None => { + error!("cancel try_handler: missing try icon"); + return; + } + }; + + let mut subsystem_subscription = match cli + .request(Request::QuerySubscribe( + "query runtime:subsystems/".to_string(), + )) + .await + { + Ok(rx) => rx, + Err(err) => { + error!( + "cancel try_handler: failed to subscribe to 'runtime:subsystems': {}", + err + ); + return; + } + }; + + let mut spn_status_subscription = match cli + .request(Request::QuerySubscribe( + "query runtime:spn/status".to_string(), + )) + .await + { + Ok(rx) => rx, + Err(err) => { + error!( + "cancel try_handler: failed to subscribe to 'runtime:spn/status': {}", + err + ); + return; + } + }; + + let mut spn_config_subscription = match cli + .request(Request::QuerySubscribe( + "query config:spn/enable".to_string(), + )) + .await + { + Ok(rx) => rx, + Err(err) => { + error!( + "cancel try_handler: failed to subscribe to 'runtime:spn/enable': {}", + err + ); + return; + } + }; + + _ = icon.set_icon(Some(Icon::Raw(BLUE_ICON.to_vec()))); + + let mut subsystems: HashMap = HashMap::new(); + let mut spn_status: String = "".to_string(); + + loop { + tokio::select! { + msg = subsystem_subscription.recv() => { + let msg = match msg { + Some(m) => m, + None => { break } + }; + + let res = match msg { + Response::Ok(key, payload) => Some((key, payload)), + Response::New(key, payload) => Some((key, payload)), + Response::Update(key, payload) => Some((key, payload)), + _ => None, + }; + + if let Some((_, payload)) = res { + match payload.parse::() { + Ok(n) => { + subsystems.insert(n.id.clone(), n); + + update_icon(icon.clone(), subsystems.clone(), spn_status.clone()); + }, + Err(err) => match err { + ParseError::JSON(err) => { + error!("failed to parse subsystem: {}", err); + } + _ => { + error!("unknown error when parsing notifications payload"); + } + }, + } + } + }, + msg = spn_status_subscription.recv() => { + let msg = match msg { + Some(m) => m, + None => { break } + }; + + let res = match msg { + Response::Ok(key, payload) => Some((key, payload)), + Response::New(key, payload) => Some((key, payload)), + Response::Update(key, payload) => Some((key, payload)), + _ => None, + }; + + if let Some((_, payload)) = res { + match payload.parse::() { + Ok(value) => { + debug!("SPN status update: {}", value.status); + spn_status = value.status.clone(); + + update_icon(icon.clone(), subsystems.clone(), spn_status.clone()); + }, + Err(err) => match err { + ParseError::JSON(err) => { + error!("failed to parse spn status value: {}", err) + }, + _ => { + error!("unknown error when parsing spn status value") + } + } + } + } + }, + msg = spn_config_subscription.recv() => { + let msg = match msg { + Some(m) => m, + None => { break } + }; + + let res = match msg { + Response::Ok(key, payload) => Some((key, payload)), + Response::New(key, payload) => Some((key, payload)), + Response::Update(key, payload) => Some((key, payload)), + _ => None, + }; + + if let Some((_, payload)) = res { + match payload.parse::() { + Ok(value) => { + let mut btn = SPN_BUTTON.lock().unwrap(); + + if let Some(btn) = &mut *btn { + if let Some(value) = value.value { + _ = btn.set_checked(value); + } else { + _ = btn.set_checked(false); + } + } + }, + Err(err) => match err { + ParseError::JSON(err) => { + error!("failed to parse config value: {}", err) + }, + _ => { + error!("unknown error when parsing config value") + } + } + } + } + } + } + } + + if let Some(btn) = &mut *(SPN_BUTTON.lock().unwrap()) { + _ = btn.set_checked(false); + } + + _ = icon.set_icon(Some(Icon::Raw(RED_ICON.to_vec()))); +} diff --git a/desktop/tauri/src-tauri/src/window.rs b/desktop/tauri/src-tauri/src/window.rs new file mode 100644 index 00000000..2059a134 --- /dev/null +++ b/desktop/tauri/src-tauri/src/window.rs @@ -0,0 +1,151 @@ +use log::{debug, error}; +use tauri::{AppHandle, Manager, Result, UserAttentionType, Window, WindowBuilder, WindowUrl}; + +use crate::portmaster::PortmasterExt; + +/// Either returns the existing "main" window or creates a new one. +/// +/// The window is not automatically shown (i.e it starts hidden). +/// If a new main window is created (i.e. the tauri app was minimized to system-tray) +/// then the window will be automatically navigated to the Portmaster UI endpoint +/// if ::websocket::is_portapi_reachable returns true. +/// +/// Either the existing or the newly created window is returned. +pub fn create_main_window(app: &AppHandle) -> Result { + let mut window = if let Some(window) = app.get_window("main") { + debug!("[tauri] main window already created"); + + window + } else { + debug!("[tauri] creating main window"); + + let res = WindowBuilder::new(app, "main", WindowUrl::App("index.html".into())) + .visible(false) + .build(); + + match res { + Ok(win) => { + win.once("tauri://error", |event| { + error!("failed to open tauri window: {}", event.payload()); + }); + + win + } + Err(err) => { + error!("[tauri] failed to create main window: {}", err.to_string()); + + return Err(err); + } + } + }; + + // If the window is not yet navigated to the Portmaster UI, do it now. + may_navigate_to_ui(&mut window, false); + + #[cfg(debug_assertions)] + if let Ok(_) = std::env::var("TAURI_SHOW_IMMEDIATELY") { + debug!("[tauri] TAURI_SHOW_IMMEDIATELY is set, opening window"); + + if let Err(err) = window.show() { + error!("[tauri] failed to show window: {}", err.to_string()); + } + } + + Ok(window) +} + +pub fn create_splash_window(app: &AppHandle) -> Result { + if let Some(window) = app.get_window("splash") { + let _ = window.show(); + Ok(window) + } else { + let window = WindowBuilder::new(app, "splash", WindowUrl::App("index.html".into())) + .center() + .closable(false) + .focused(true) + .resizable(false) + .visible(true) + .title("Portmaster") + .inner_size(600.0, 250.0) + .build()?; + + let _ = window.request_user_attention(Some(UserAttentionType::Informational)); + + Ok(window) + } +} + +pub fn close_splash_window(app: &AppHandle) -> Result<()> { + if let Some(window) = app.get_window("splash") { + return window.close(); + } + return Err(tauri::Error::WindowNotFound); +} + +/// Opens a window for the tauri application. +/// +/// If the main window has already been created, it is instructed to +/// show even if we're currently not connected to Portmaster. +/// This is safe since the main-window will only be created if Portmaster API +/// was reachable so the angular application must have finished bootstrapping. +/// +/// If there's not main window and the Portmaster API is reachable we create a new +/// main window. +/// +/// If the Portmaster API is unreachable and there's no main window yet, we show the +/// splash-screen window. +pub fn open_window(app: &AppHandle) -> Result { + if app.portmaster().is_reachable() { + match app.get_window("main") { + Some(win) => { + app.portmaster().show_window(); + + Ok(win) + } + None => { + app.portmaster().show_window(); + + create_main_window(app) + } + } + } else { + debug!("Show splash screen"); + create_splash_window(app) + } +} + +/// If the Portmaster Websocket database API is reachable the window will be navigated +/// to the HTTP endpoint of Portmaster to load the UI from there. +/// +/// Note that only happens if the window URL does not already point to the PM API. +/// +/// In #[cfg(debug_assertions)] the TAURI_PM_URL environment variable will be used +/// if set. +/// Otherwise or in release builds, it will be navigated to http://127.0.0.1:817. +pub fn may_navigate_to_ui(win: &mut Window, force: bool) { + if !win.app_handle().portmaster().is_reachable() && !force { + error!("[tauri] portmaster API is not reachable, not navigating"); + + return; + } + + if force || cfg!(debug_assertions) || win.url().host_str() != Some("localhost") { + #[cfg(debug_assertions)] + if let Ok(target_url) = std::env::var("TAURI_PM_URL") { + debug!("[tauri] navigating to {}", target_url); + + win.navigate(target_url.parse().unwrap()); + + return; + } + + #[cfg(debug_assertions)] + { + debug!("[tauri] navigating to http://localhost:4200"); + win.navigate("http://localhost:4200".parse().unwrap()); + } + + #[cfg(not(debug_assertions))] + win.navigate("http://localhost:817".parse().unwrap()); + } +} diff --git a/desktop/tauri/src-tauri/src/xdg/mod.rs b/desktop/tauri/src-tauri/src/xdg/mod.rs new file mode 100644 index 00000000..b1fa9089 --- /dev/null +++ b/desktop/tauri/src-tauri/src/xdg/mod.rs @@ -0,0 +1,585 @@ +use cached::proc_macro::once; +use dataurl::DataUrl; +use gdk_pixbuf::{Pixbuf, PixbufError}; +use gtk_sys::{ + gtk_icon_info_free, gtk_icon_info_get_filename, gtk_icon_theme_get_default, + gtk_icon_theme_lookup_icon, GtkIconTheme, +}; +use log::{debug, error}; +use std::collections::HashMap; +use std::ffi::c_int; +use std::ffi::{CStr, CString}; +use std::io; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, RwLock}; +use std::{ + env, fs, + io::{Error, ErrorKind}, +}; +use thiserror::Error; + +use dirs; +use ini::{Ini, ParseOption}; + +static mut GTK_DEFAULT_THEME: Option<*mut GtkIconTheme> = None; + +lazy_static! { + static ref APP_INFO_CACHE: Arc>>> = + Arc::new(RwLock::new(HashMap::new())); +} + +#[derive(Debug, Error)] +pub enum LookupError { + #[error(transparent)] + IoError(#[from] std::io::Error), +} + +pub type Result = std::result::Result; + +#[derive(Clone, serde::Serialize)] +pub struct AppInfo { + pub icon_name: String, + pub app_name: String, + pub icon_dataurl: String, + pub comment: String, +} + +impl Default for AppInfo { + fn default() -> Self { + AppInfo { + icon_dataurl: "".to_string(), + icon_name: "".to_string(), + app_name: "".to_string(), + comment: "".to_string(), + } + } +} + +#[derive(Clone, serde::Serialize, Debug)] +pub struct ProcessInfo { + pub exec_path: String, + pub cmdline: String, + pub pid: i64, + pub matching_path: String, +} + +impl std::fmt::Display for ProcessInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} (cmdline={}) (pid={}) (matching_path={})", + self.exec_path, self.cmdline, self.pid, self.matching_path + ) + } +} + +pub fn get_app_info(process_info: ProcessInfo) -> Result { + { + let cache = APP_INFO_CACHE.read().unwrap(); + + if let Some(value) = cache.get(process_info.exec_path.as_str()) { + match value { + Some(app_info) => return Ok(app_info.clone()), + None => { + return Err(LookupError::IoError(io::Error::new( + io::ErrorKind::NotFound, + "not found", + ))) + } + } + } + } + + let mut needles = Vec::new(); + if !process_info.exec_path.is_empty() { + needles.push(process_info.exec_path.as_str()) + } + if !process_info.cmdline.is_empty() { + needles.push(process_info.cmdline.as_str()) + } + if !process_info.matching_path.is_empty() { + needles.push(process_info.matching_path.as_str()) + } + + // sort and deduplicate + needles.sort(); + needles.dedup(); + + debug!("Searching app info for {:?}", process_info); + + let mut desktop_files = Vec::new(); + for dir in get_application_directories()? { + let mut files = find_desktop_files(dir.as_path())?; + desktop_files.append(&mut files); + } + + let mut matches = Vec::new(); + for needle in needles.clone() { + debug!("Trying needle {} on exec path", needle); + + match try_get_app_info(needle, CheckType::Exec, &desktop_files) { + Ok(mut result) => { + matches.append(&mut result); + } + Err(LookupError::IoError(ioerr)) => { + if ioerr.kind() != ErrorKind::NotFound { + return Err(ioerr.into()); + } + } + }; + + match try_get_app_info(needle, CheckType::Name, &desktop_files) { + Ok(mut result) => { + matches.append(&mut result); + } + Err(LookupError::IoError(ioerr)) => { + if ioerr.kind() != ErrorKind::NotFound { + return Err(ioerr.into()); + } + } + }; + } + + if matches.is_empty() { + APP_INFO_CACHE + .write() + .unwrap() + .insert(process_info.exec_path, None); + + Err(Error::new(ErrorKind::NotFound, format!("failed to find app info")).into()) + } else { + // sort matches by length + matches.sort_by(|a, b| a.1.cmp(&b.1)); + + for mut info in matches { + match get_icon_as_png_dataurl(&info.0.icon_name, 32) { + Ok(du) => { + debug!( + "[xdg] best match for {:?} is {:?} with len {}", + process_info, info.0.icon_name, info.1 + ); + + info.0.icon_dataurl = du.1; + + APP_INFO_CACHE + .write() + .unwrap() + .insert(process_info.exec_path, Some(info.0.clone())); + + return Ok(info.0); + } + Err(err) => { + error!( + "{}: failed to get icon: {}", + info.0.icon_name, + err.to_string() + ); + } + }; + } + + Err(Error::new(ErrorKind::NotFound, format!("failed to find app info")).into()) + } +} + +/// Returns a vector of application directories that are expected +/// to contain all .desktop files the current user has access to. +/// The result of this function is cached for 5 minutes as it's not expected +/// that application directories actually change. +#[once(time = 300, sync_writes = true, result = true)] +fn get_application_directories() -> Result> { + let xdg_home = match env::var_os("XDG_DATA_HOME") { + Some(path) => PathBuf::from(path), + None => { + let home = dirs::home_dir() + .ok_or(Error::new(ErrorKind::Other, "Failed to get home directory"))?; + + home.join(".local/share") + } + }; + + let extra_application_dirs = match env::var_os("XDG_DATA_DIRS") { + Some(paths) => env::split_paths(&paths).map(PathBuf::from).collect(), + None => { + // Fallback if XDG_DATA_DIRS is not set. If it's set, it normally already contains /usr/share and + // /usr/local/share + vec![ + PathBuf::from("/usr/share"), + PathBuf::from("/usr/local/share"), + ] + } + }; + + let mut app_dirs = Vec::new(); + for extra_dir in extra_application_dirs { + app_dirs.push(extra_dir.join("applications")); + } + + app_dirs.push(xdg_home.join("applications")); + + Ok(app_dirs) +} + +// TODO(ppacher): cache the result of find_desktop_files as well. +// Though, seems like we cannot use the #[cached::proc_macro::cached] or #[cached::proc_macro::once] macros here +// because [`Result>>`] does not implement [`Clone`] +fn find_desktop_files(path: &Path) -> Result> { + match path.read_dir() { + Ok(files) => { + let desktop_files = files + .filter_map(|entry| entry.ok()) + .filter(|entry| match entry.file_type() { + Ok(ft) => ft.is_file() || ft.is_symlink(), + _ => false, + }) + .filter(|entry| entry.file_name().to_string_lossy().ends_with(".desktop")) + .collect::>(); + + Ok(desktop_files) + } + Err(err) => { + // We ignore NotFound errors here because not all application + // directories need to exist. + if err.kind() == ErrorKind::NotFound { + Ok(Vec::new()) + } else { + Err(err.into()) + } + } + } +} + +enum CheckType { + Name, + Exec, +} + +fn try_get_app_info( + needle: &str, + check: CheckType, + desktop_files: &Vec, +) -> Result> { + let path = PathBuf::from(needle); + + let file_name = path.as_path().file_name().unwrap_or_default().to_str(); + + let mut result = Vec::new(); + + for file in desktop_files { + let content = Ini::load_from_file_opt( + file.path(), + ParseOption { + enabled_escape: false, + enabled_quote: true, + }, + ) + .map_err(|err| Error::new(ErrorKind::Other, err.to_string()))?; + + let desktop_section = match content.section(Some("Desktop Entry")) { + Some(section) => section, + None => { + continue; + } + }; + + let matches = match check { + CheckType::Name => { + let name = match desktop_section.get("Name") { + Some(name) => name, + None => { + continue; + } + }; + + if let Some(file_name) = file_name { + if name.to_lowercase().contains(file_name) { + file_name.len() + } else { + 0 + } + } else { + 0 + } + } + CheckType::Exec => { + let exec = match desktop_section.get("Exec") { + Some(exec) => exec, + None => { + continue; + } + }; + + if exec.to_lowercase().contains(needle) { + needle.len() + } else if let Some(file_name) = file_name { + if exec.to_lowercase().starts_with(file_name) { + file_name.len() + } else { + 0 + } + } else { + 0 + } + } + }; + + if matches > 0 { + debug!( + "[xdg] found matching desktop for needle {} file at {}", + needle, + file.path().to_string_lossy() + ); + + let info = parse_app_info(desktop_section); + + result.push((info, matches)); + } + } + + if result.len() > 0 { + Ok(result) + } else { + Err(Error::new(ErrorKind::NotFound, "no matching .desktop files found").into()) + } +} + +fn parse_app_info(props: &ini::Properties) -> AppInfo { + AppInfo { + icon_dataurl: "".to_string(), + app_name: props.get("Name").unwrap_or_default().to_string(), + comment: props.get("Comment").unwrap_or_default().to_string(), + icon_name: props.get("Icon").unwrap_or_default().to_string(), + } +} + +fn get_icon_as_png_dataurl(name: &str, size: i8) -> Result<(String, String)> { + unsafe { + if GTK_DEFAULT_THEME.is_none() { + let theme = gtk_icon_theme_get_default(); + if theme.is_null() { + debug!("You have to initialize GTK!"); + return Err(Error::new(ErrorKind::Other, "You have to initialize GTK!").into()); + } + + let theme = gtk_icon_theme_get_default(); + GTK_DEFAULT_THEME = Some(theme); + } + } + + let mut icons = Vec::new(); + + // push the name + icons.push(name); + + // if we don't find the icon by it's name and it includes an extension, + // drop the extension and try without. + let name_without_ext; + if let Some(ext) = PathBuf::from(name).extension() { + let ext = ext.to_str().unwrap(); + + let mut ext_dot = String::from(".").to_owned(); + ext_dot.push_str(ext); + + name_without_ext = name.replace(ext_dot.as_str(), ""); + icons.push(name_without_ext.as_str()); + } else { + name_without_ext = String::from(name); + } + + // The xdg-desktop icon specification allows a fallback for icons that contains dashes. + // i.e. the following lookup order is used: + // - network-wired-secure + // - network-wired + // - network + // + name_without_ext + .split("-") + .for_each(|part| icons.push(part)); + + for name in icons { + debug!("trying to load icon {}", name); + + unsafe { + let c_str = CString::new(name).unwrap(); + + let icon_info = gtk_icon_theme_lookup_icon( + GTK_DEFAULT_THEME.unwrap(), + c_str.as_ptr() as *const i8, + size as c_int, + 0, + ); + if icon_info.is_null() { + error!("failed to lookup icon {}", name); + + continue; + } + + let filename = gtk_icon_info_get_filename(icon_info); + + let filename = CStr::from_ptr(filename).to_str().unwrap().to_string(); + + gtk_icon_info_free(icon_info); + + match read_and_convert_pixbuf(filename.clone()) { + Ok(pb) => return Ok((filename, pb)), + Err(err) => { + error!("failed to load icon from {}: {}", filename, err.to_string()); + + continue; + } + } + } + } + + Err(Error::new(ErrorKind::NotFound, "failed to find icon").into()) +} + +/* +fn get_icon_as_file_2(ext: &str, size: i32) -> io::Result<(String, Vec)> { + let result: String; + let buf: Vec; + + unsafe { + let filename = CString::new(ext).unwrap(); + let null: u8 = 0; + let p_null = &null as *const u8; + let nullsize: usize = 0; + let mut res = 0; + let p_res = &mut res as *mut i32; + let p_res = gio_sys::g_content_type_guess(filename.as_ptr(), p_null, nullsize, p_res); + let icon = gio_sys::g_content_type_get_icon(p_res); + g_free(p_res as *mut c_void); + if DEFAULT_THEME.is_none() { + let theme = gtk_icon_theme_get_default(); + if theme.is_null() { + println!("You have to initialize GTK!"); + return Err(io::Error::new(io::ErrorKind::Other, "You have to initialize GTK!")) + } + let theme = gtk_icon_theme_get_default(); + DEFAULT_THEME = Some(theme); + } + let icon_names = gio_sys::g_themed_icon_get_names(icon as *mut GThemedIcon) as *mut *const i8; + let icon_info = gtk_icon_theme_choose_icon(DEFAULT_THEME.unwrap(), icon_names, size, GTK_ICON_LOOKUP_NO_SVG); + let filename = gtk_icon_info_get_filename(icon_info); + + gtk_icon_info_free(icon_info); + + result = CStr::from_ptr(filename).to_str().unwrap().to_string(); + + buf = match read_and_convert_pixbuf(result.clone()) { + Ok(pb) => pb, + Err(_) => Vec::new(), + }; + + g_object_unref(icon as *mut GObject); + } + + Ok((result, buf)) + +} +*/ + +fn read_and_convert_pixbuf(result: String) -> std::result::Result { + let pixbuf = match Pixbuf::from_file(result.clone()) { + Ok(data) => Ok(data), + Err(err) => { + error!("failed to load icon pixbuf: {}", err.to_string()); + + Pixbuf::from_resource(result.clone().as_str()) + } + }; + + match pixbuf { + Ok(data) => match data.save_to_bufferv("png", &[]) { + Ok(data) => { + let mut du = DataUrl::new(); + + du.set_media_type(Some("image/png".to_string())); + du.set_data(&data); + + Ok(du.to_string()) + } + Err(err) => { + return Err(glib::Error::new( + PixbufError::Failed, + err.to_string().as_str(), + )); + } + }, + Err(err) => Err(err), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ctor::ctor; + use log::warn; + use which::which; + + // Use the ctor create to setup a global initializer before our tests are executed. + #[ctor] + fn init() { + // we need to initialize GTK before running our tests. + // This is only required when unit tests are executed as + // GTK will otherwise be initialize by Tauri. + + gtk::init().expect("failed to initialize GTK for tests") + } + + #[test] + fn test_find_info_success() { + // we expect at least one of the following binaries to be installed + // on a linux system + let test_binaries = vec![ + "vim", // vim is mostly bundled with a .desktop file + "blueman-manager", // blueman-manager is the default bluetooth manager on most DEs + "nautilus", // nautlis: file-manager on GNOME DE + "thunar", // thunar: file-manager on XFCE + "dolphin", // dolphin: file-manager on KDE + ]; + + let mut bin_found = false; + + for cmd in test_binaries { + match which(cmd) { + Ok(bin) => { + bin_found = true; + + let bin = bin.to_string_lossy().to_string(); + + let result = get_app_info(ProcessInfo { + cmdline: cmd.to_string(), + exec_path: bin.clone(), + matching_path: bin.clone(), + pid: 0, + }) + .expect( + format!( + "expected to find app info for {} ({})", + bin, + cmd.to_string() + ) + .as_str(), + ); + + let empty_string = String::from(""); + + // just make sure all fields are populated + assert_ne!(result.app_name, empty_string); + assert_ne!(result.comment, empty_string); + assert_ne!(result.icon_name, empty_string); + assert_ne!(result.icon_dataurl, empty_string); + } + Err(_) => { + // binary not found + continue; + } + } + } + + if !bin_found { + warn!("test_find_info_success: no test binary found, test was skipped") + } + } +} diff --git a/desktop/tauri/src-tauri/tauri.conf.json b/desktop/tauri/src-tauri/tauri.conf.json new file mode 100644 index 00000000..0fca6f4f --- /dev/null +++ b/desktop/tauri/src-tauri/tauri.conf.json @@ -0,0 +1,106 @@ +{ + "build": { + "beforeDevCommand": { + "script": "npm run tauri-dev", + "cwd": "../../angular", + "wait": false + }, + "devPath": "http://localhost:4100", + "distDir": "../../angular/dist/tauri-builtin", + "withGlobalTauri": true + }, + "package": { + "productName": "Portmaster", + "version": "0.1.0" + }, + "plugins": { + "cli": { + "args": [ + { + "short": "d", + "name": "data", + "description": "Path to the installation directory", + "takesValue": true + }, + { + "short": "b", + "name": "background", + "description": "Start in the background without opening a window" + }, + { + "name": "with-notifications", + "description": "Enable experimental notifications via Tauri. Replaces the notifier app." + }, + { + "name": "with-prompts", + "description": "Enable experimental prompt support via Tauri. Replaces the notifier app." + } + ] + } + }, + "tauri": { + "bundle": { + "active": true, + "category": "Utility", + "copyright": "", + "deb": { + "depends": [] + }, + "externalBin": [ + "binaries/portmaster-start", + "binaries/portmaster-core" + ], + "icon": [ + "../assets/icons/pm_dark_512.png", + "../assets/icons/pm_dark_512.ico", + "../assets/icons/pm_light_512.png", + "../assets/icons/pm_light_512.ico" + ], + "identifier": "io.safing.portmaster", + "longDescription": "", + "macOS": { + "entitlements": null, + "exceptionDomain": "", + "frameworks": [], + "providerShortName": null, + "signingIdentity": null + }, + "resources": [], + "shortDescription": "", + "targets": [ + "deb", + "appimage", + "nsis", + "msi", + "app" + ], + "windows": { + "certificateThumbprint": null, + "digestAlgorithm": "sha256", + "timestampUrl": "" + } + }, + "security": { + "csp": null, + "dangerousRemoteDomainIpcAccess": [ + { + "windows": [ + "main", + "prompt" + ], + "plugins": [ + "shell", + "os", + "clipboard-manager", + "event", + "window", + "cli", + "portmaster" + ], + "domain": "localhost" + } + ] + }, + "windows": [] + } +} \ No newline at end of file From 458336006fc09bb7891d0e8b4db638e38b4867c9 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 26 Mar 2024 15:38:47 +0100 Subject: [PATCH 06/35] Update Earthfile to hopefully get cross-compilation for rust to work --- Earthfile | 191 +++++++++++++++++------- cmds/portmaster-core/build | 60 -------- cmds/portmaster-core/pack | 123 --------------- desktop/tauri/.cargo/config.toml | 7 + desktop/tauri/src-tauri/Cross.toml | 7 + desktop/tauri/src-tauri/src/xdg/mod.rs | 2 +- desktop/tauri/src-tauri/tauri.conf.json | 4 +- 7 files changed, 152 insertions(+), 242 deletions(-) delete mode 100755 cmds/portmaster-core/build delete mode 100755 cmds/portmaster-core/pack create mode 100644 desktop/tauri/.cargo/config.toml create mode 100644 desktop/tauri/src-tauri/Cross.toml diff --git a/Earthfile b/Earthfile index ad1893d9..f2c2af91 100644 --- a/Earthfile +++ b/Earthfile @@ -8,6 +8,7 @@ ARG --global outputDir = "./dist" # The list of rust targets we support. They will be automatically converted # to GOOS, GOARCH and GOARM when building go binaries. See the +RUST_TO_GO_ARCH_STRING # helper method at the bottom of the file. + ARG --global architectures = "x86_64-unknown-linux-gnu" \ "aarch64-unknown-linux-gnu" \ "armv7-unknown-linux-gnueabihf" \ @@ -22,6 +23,9 @@ go-deps: FROM golang:${go_version}-${distro} WORKDIR /go-workdir + # We need the git cli to extract version information for go-builds + RUN apk add git + # These cache dirs will be used in later test and build targets # to persist cached go packages. # @@ -54,6 +58,18 @@ go-base: # ./assets COPY assets ./assets + # Copy the git folder and extract version information + COPY .git ./.git + + LET version = $(git tag --points-at) + IF [ "${version}" = "" ] + SET version = $(git describe --tags --abbrev=0) + END + ENV VERSION="${version}" + + LET source = $(git remote -v | cut -f2 | cut -d" " -f1 | head -n 1) + ENV SOURCE="${source}" + # updates all go dependencies and runs go mod tidy, saving go.mod and go.sum locally. update-go-deps: FROM +go-base @@ -82,9 +98,6 @@ build-go: ARG GOARM ARG CMDS=portmaster-start portmaster-core hub notifier - # Get the current version - DO +GET_VERSION - CACHE --sharing shared "$GOCACHE" CACHE --sharing shared "$GOMODCACHE" @@ -97,12 +110,12 @@ build-go: # Build all go binaries from the specified in CMDS FOR bin IN $CMDS - RUN go build -o "/tmp/build/" ./cmds/${bin} + RUN go build -ldflags="-X github.com/safing/portbase/info.version=${VERSION} -X github.com/safing/portbase/info.buildSource=${SOURCE}" -o "/tmp/build/" ./cmds/${bin} END - FOR bin IN $(ls -1 "/tmp/build/") - DO +GO_ARCH_STRING --goos="${GOOS}" --goarch="${GOARCH}" --goarm="${GOARM}" + DO +GO_ARCH_STRING --goos="${GOOS}" --goarch="${GOARCH}" --goarm="${GOARM}" + FOR bin IN $(ls -1 "/tmp/build/") SAVE ARTIFACT "/tmp/build/${bin}" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/${bin}" END @@ -129,7 +142,8 @@ test-go: END test-go-all-platforms: - LOCALLY + FROM alpine:3.18 + FOR arch IN ${architectures} DO +RUST_TO_GO_ARCH_STRING --rustTarget="${arch}" BUILD +test-go --GOARCH="${GOARCH}" --GOOS="${GOOS}" --GOARM="${GOARM}" @@ -137,9 +151,19 @@ test-go-all-platforms: # Builds portmaster-start, portmaster-core, hub and notifier for all supported platforms build-go-release: - LOCALLY + FROM alpine:3.18 + FOR arch IN ${architectures} DO +RUST_TO_GO_ARCH_STRING --rustTarget="${arch}" + + IF [ -z GOARCH ] + RUN echo "Failed to extract GOARCH for ${arch}"; exit 1 + END + + IF [ -z GOOS ] + RUN echo "Failed to extract GOOS for ${arch}"; exit 1 + END + BUILD +build-go --GOARCH="${GOARCH}" --GOOS="${GOOS}" --GOARM="${GOARM}" END @@ -207,7 +231,12 @@ angular-dev: rust-base: FROM rust:1.76-bookworm + RUN dpkg --add-architecture armhf + RUN dpkg --add-architecture arm64 + RUN apt-get update -qq + + # Tools and libraries required for cross-compilation RUN apt-get install --no-install-recommends -qq \ autoconf \ autotools-dev \ @@ -215,28 +244,66 @@ rust-base: clang \ cmake \ bsdmainutils \ + gcc-multilib \ + linux-libc-dev \ + linux-libc-dev-amd64-cross \ + linux-libc-dev-arm64-cross \ + linux-libc-dev-armel-cross \ + linux-libc-dev-armhf-cross \ + build-essential \ + curl \ + wget \ + file \ + libsoup-3.0-dev \ + libwebkit2gtk-4.1-dev + + # Install library dependencies for all supported architectures + # required for succesfully linking. + FOR arch IN amd64 arm64 armhf + RUN apt-get install --no-install-recommends -qq \ + libsoup-3.0-0:${arch} \ + libwebkit2gtk-4.1-0:${arch} \ + libssl3:${arch} \ + libayatana-appindicator3-1:${arch} \ + librsvg2-bin:${arch} \ + libgtk-3-0:${arch} \ + libjavascriptcoregtk-4.1-0:${arch} \ + libssl-dev:${arch} \ + libayatana-appindicator3-dev:${arch} \ + librsvg2-dev:${arch} \ + libgtk-3-dev:${arch} \ + libjavascriptcoregtk-4.1-dev:${arch} + END + + # Note(ppacher): I've no idea why we need to explicitly create those symlinks: + # Some how all the other libs work but libsoup and libwebkit2gtk do not create the link file + RUN cd /usr/lib/aarch64-linux-gnu && \ + ln -s libwebkit2gtk-4.1.so.0 libwebkit2gtk-4.1.so && \ + ln -s libsoup-3.0.so.0 libsoup-3.0.so + + RUN cd /usr/lib/arm-linux-gnueabihf && \ + ln -s libwebkit2gtk-4.1.so.0 libwebkit2gtk-4.1.so && \ + ln -s libsoup-3.0.so.0 libsoup-3.0.so + + # For what ever reason trying to install the gcc compilers together with the above + # command makes apt fail due to conflicts with gcc-multilib. Installing in a separate + # step seems to work ... + RUN apt-get install --no-install-recommends -qq \ g++-mingw-w64-x86-64 \ gcc-aarch64-linux-gnu \ gcc-arm-none-eabi \ gcc-arm-linux-gnueabi \ gcc-arm-linux-gnueabihf \ - libgtk-3-dev \ - libjavascriptcoregtk-4.1-dev \ - libsoup-3.0-dev \ - libwebkit2gtk-4.1-dev \ - build-essential \ - curl \ - wget \ - file \ - libssl-dev \ - libayatana-appindicator3-dev \ - librsvg2-dev + libc6-dev-arm64-cross \ + libc6-dev-armel-cross \ + libc6-dev-armhf-cross \ + libc6-dev-amd64-cross # Add some required rustup components RUN rustup component add clippy RUN rustup component add rustfmt - # Install toolchains and targets + # Install architecture targets FOR arch IN ${architectures} RUN rustup target add ${arch} END @@ -246,6 +313,9 @@ rust-base: # For now we need tauri-cli 1.5 for bulding DO rust+CARGO --args="install tauri-cli --version ^1.5.11" + # Required for cross compilation to work. + ENV PKG_CONFIG_ALLOW_CROSS=1 + tauri-src: FROM +rust-base @@ -268,28 +338,34 @@ build-tauri: # we need to do some magic here because tauri expects the binaries to include the rust target tripple. # We already knwo that triple because it's a required argument. From that triple, we use +RUST_TO_GO_ARCH_STRING # function from below to parse the triple and guess wich GOOS and GOARCH we need. - IF [ "${bundle}" != "none" ] - RUN mkdir /tmp/gobuild - RUN mkdir ./binaries + RUN mkdir /tmp/gobuild + RUN mkdir ./binaries - DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}" - RUN echo "GOOS=${GOOS} GOARCH=${GOARCH} GOARM=${GOARM} GO_ARCH_STRING=${GO_ARCH_STRING}" + DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}" + RUN echo "GOOS=${GOOS} GOARCH=${GOARCH} GOARM=${GOARM} GO_ARCH_STRING=${GO_ARCH_STRING}" - COPY (+build-go/output --GOOS="${GOOS}" --CMDS="portmaster-start portmaster-core" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild + # Our tauri app has externalBins configured so tauri will try to embed them when it finished compiling + # the app. Make sure we copy portmaster-start and portmaster-core in all architectures supported. + # See documentation for externalBins for more information on how tauri searches for the binaries. - LET dest="" - FOR bin IN $(ls /tmp/gobuild) - SET dest="./binaries/${bin}-${target}" + COPY (+build-go/output --GOOS="${GOOS}" --CMDS="portmaster-start portmaster-core" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild - IF [ -z "${bin##*.exe}" ] - SET dest = "./binaries/${bin%.*}-${target}.exe" - END + # Place them in the correct folder with the rust target tripple attached. + LET dest="" + FOR bin IN $(ls /tmp/gobuild) + SET dest="./binaries/${bin}-${target}" - RUN echo "Copying ${bin} to ${dest}" - RUN cp "/tmp/gobuild/${bin}" "${dest}" + IF [ -z "${bin##*.exe}" ] + SET dest = "./binaries/${bin%.*}-${target}.exe" END + + RUN echo "Copying ${bin} to ${dest}" + RUN cp "/tmp/gobuild/${bin}" "${dest}" END + # Just for debugging ... + RUN ls -R ./binaries + # The following is exected to work but doesn't. for whatever reason cargo-sweep errors out on the windows-toolchain. # # DO rust+CARGO --args="tauri build --bundles none --ci --target=${target}" --output="release/[^/\.]+" @@ -300,10 +376,23 @@ build-tauri: RUN --mount=$EARTHLY_RUST_TARGET_CACHE cargo tauri build --bundles "${bundle}" --ci --target="${target}" DO rust+COPY_OUTPUT --output="${output}" + # BUG(cross-compilation): + # + # The above command seems to correctly compile for all architectures we want to support but fails during + # linking since the target libaries are not available for the requested platforms. Maybe we need to download + # the, manually ... + # + # The earthly rust lib also has support for using cross-rs for cross-compilation but that fails due to the + # fact that cross-rs base docker images used for building are heavily outdated (latest = ubunut:16.0, main = ubuntu:20.04) + # which does not ship recent enough glib versions (our glib dependency needs glib>2.70 but ubunut:20.04 only ships 2.64) + # + # The following would use the CROSS function from the earthly lib, this + # DO rust+CROSS --target="${target}" + RUN ls target tauri-release: - LOCALLY + FROM alpine:3.18 ARG bundle="none" @@ -311,9 +400,18 @@ tauri-release: BUILD +build-tauri --target="${arch}" --bundle="${bundle}" END -release: +build-all: BUILD +build-go-release - BUILD +angular-release + BUILD +tauri-release + +release: + LOCALLY + + IF ! git diff --quiet + RUN echo -e "\033[1;31m Refusing to release a dirty git repository. Please commit your local changes first! \033[0m" ; exit 1 + END + + BUILD +build-all # Takes GOOS, GOARCH and optionally GOARM and creates a string representation for file-names. @@ -375,22 +473,3 @@ RUST_TO_GO_ARCH_STRING: ENV GOARM="${goarm}" DO +GO_ARCH_STRING --goos="${goos}" --goarch="${goarch}" --goarm="${goarm}" - -GET_VERSION: - FUNCTION - LOCALLY - - LET VERSION=$(git tag --points-at) - IF [ -z "${VERSION}"] - SET VERSION=$(git describe --tags --abbrev=0)§dev§build - ELSE IF ! git diff --quite - SET VERSION="${VERSION}§dev§build" - END - - RUN echo "Version is ${VERSION}" - ENV VERSION="${VERSION}" - -test: - LOCALLY - - DO +GET_VERSION \ No newline at end of file diff --git a/cmds/portmaster-core/build b/cmds/portmaster-core/build deleted file mode 100755 index 355e661e..00000000 --- a/cmds/portmaster-core/build +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -# get build data -if [[ "$BUILD_COMMIT" == "" ]]; then - BUILD_COMMIT=$(git describe --all --long --abbrev=99 --dirty 2>/dev/null) -fi -if [[ "$BUILD_USER" == "" ]]; then - BUILD_USER=$(id -un) -fi -if [[ "$BUILD_HOST" == "" ]]; then - BUILD_HOST=$(hostname -f) -fi -if [[ "$BUILD_DATE" == "" ]]; then - BUILD_DATE=$(date +%d.%m.%Y) -fi -if [[ "$BUILD_SOURCE" == "" ]]; then - BUILD_SOURCE=$(git remote -v | grep origin | cut -f2 | cut -d" " -f1 | head -n 1) -fi -if [[ "$BUILD_SOURCE" == "" ]]; then - BUILD_SOURCE=$(git remote -v | cut -f2 | cut -d" " -f1 | head -n 1) -fi -BUILD_BUILDOPTIONS=$(echo $* | sed "s/ /§/g") - -# check -if [[ "$BUILD_COMMIT" == "" ]]; then - echo "could not automatically determine BUILD_COMMIT, please supply manually as environment variable." - exit 1 -fi -if [[ "$BUILD_USER" == "" ]]; then - echo "could not automatically determine BUILD_USER, please supply manually as environment variable." - exit 1 -fi -if [[ "$BUILD_HOST" == "" ]]; then - echo "could not automatically determine BUILD_HOST, please supply manually as environment variable." - exit 1 -fi -if [[ "$BUILD_DATE" == "" ]]; then - echo "could not automatically determine BUILD_DATE, please supply manually as environment variable." - exit 1 -fi -if [[ "$BUILD_SOURCE" == "" ]]; then - echo "could not automatically determine BUILD_SOURCE, please supply manually as environment variable." - exit 1 -fi - -echo "Please notice, that this build script includes metadata into the build." -echo "This information is useful for debugging and license compliance." -echo "Run the compiled binary with the -version flag to see the information included." - -if [[ $1 == "dev" ]]; then - shift - export CGO_ENABLED=1 - DEV="-race" -else - export CGO_ENABLED=0 -fi - -# build -BUILD_PATH="github.com/safing/portbase/info" -go build $DEV -ldflags "-X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" "$@" diff --git a/cmds/portmaster-core/pack b/cmds/portmaster-core/pack deleted file mode 100755 index 5bdc4c6f..00000000 --- a/cmds/portmaster-core/pack +++ /dev/null @@ -1,123 +0,0 @@ -#!/bin/bash - -baseDir="$( cd "$(dirname "$0")" && pwd )" -cd "$baseDir" - -COL_OFF="\033[0m" -COL_BOLD="\033[01;01m" -COL_RED="\033[31m" -COL_GREEN="\033[32m" -COL_YELLOW="\033[33m" - -destDirPart1="../../dist" -destDirPart2="core" - -function prep { - # output - output="portmaster-core" - # get version - version=$(grep "info.Set" main.go | cut -d'"' -f4) - # build versioned file name - filename="portmaster-core_v${version//./-}" - # platform - platform="${GOOS}_${GOARCH}" - if [[ $GOOS == "windows" ]]; then - filename="${filename}.exe" - output="${output}.exe" - fi - # build destination path - destPath=${destDirPart1}/${platform}/${destDirPart2}/$filename -} - -function check { - prep - - # check if file exists - if [[ -f $destPath ]]; then - echo "[core] $platform v$version already built" - else - echo -e "${COL_BOLD}[core] $platform v$version${COL_OFF}" - fi -} - -function build { - prep - - # check if file exists - if [[ -f $destPath ]]; then - echo "[core] $platform already built in v$version, skipping..." - return - fi - - # build - ./build - if [[ $? -ne 0 ]]; then - echo -e "\n${COL_BOLD}[core] $platform v$version: ${COL_RED}BUILD FAILED.${COL_OFF}" - exit 1 - fi - mkdir -p $(dirname $destPath) - cp $output $destPath - echo -e "\n${COL_BOLD}[core] $platform v$version: ${COL_GREEN}successfully built.${COL_OFF}" -} - -function reset { - prep - - # delete if file exists - if [[ -f $destPath ]]; then - rm $destPath - echo "[core] $platform v$version deleted." - fi -} - -function check_all { - GOOS=linux GOARCH=amd64 check - GOOS=windows GOARCH=amd64 check - GOOS=darwin GOARCH=amd64 check - GOOS=linux GOARCH=arm64 check - GOOS=windows GOARCH=arm64 check - GOOS=darwin GOARCH=arm64 check -} - -function build_all { - GOOS=linux GOARCH=amd64 build - GOOS=windows GOARCH=amd64 build - GOOS=darwin GOARCH=amd64 build - GOOS=linux GOARCH=arm64 build - GOOS=windows GOARCH=arm64 build - GOOS=darwin GOARCH=arm64 build -} - -function reset_all { - GOOS=linux GOARCH=amd64 reset - GOOS=windows GOARCH=amd64 reset - GOOS=darwin GOARCH=amd64 reset - GOOS=linux GOARCH=arm64 reset - GOOS=windows GOARCH=arm64 reset - GOOS=darwin GOARCH=arm64 reset -} - -case $1 in - "check" ) - check_all - ;; - "build" ) - build_all - ;; - "reset" ) - reset_all - ;; - * ) - echo "" - echo "build list:" - echo "" - check_all - echo "" - read -p "press [Enter] to start building" x - echo "" - build_all - echo "" - echo "finished building." - echo "" - ;; -esac diff --git a/desktop/tauri/.cargo/config.toml b/desktop/tauri/.cargo/config.toml new file mode 100644 index 00000000..707bafa6 --- /dev/null +++ b/desktop/tauri/.cargo/config.toml @@ -0,0 +1,7 @@ +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" +rustflags = ["-C", "link-args=-L/usr/lib/aarch64-linux-gnu/"] + +[target.armv7-unknown-linux-gnueabihf] +linker = "arm-linux-gnueabihf-gcc" +rustflags = ["-C", "link-args=-L/usr/lib/arm-linux-gnueabihf/"] diff --git a/desktop/tauri/src-tauri/Cross.toml b/desktop/tauri/src-tauri/Cross.toml new file mode 100644 index 00000000..28e01087 --- /dev/null +++ b/desktop/tauri/src-tauri/Cross.toml @@ -0,0 +1,7 @@ +[target.aarch64-unknown-linux-gnu] +image = "ghcr.io/cross-rs/aarch64-unknown-linux-gnu:main" + +pre-build = [ + "dpkg --add-architecture $CROSS_DEB_ARCH", + "apt-get update && apt-get --assume-yes install libssl-dev:$CROSS_DEB_ARCH libjavascriptcoregtk-4.0-dev:$CROSS_DEB_ARCH librsvg2-dev libayatana-appindicator3-dev libwebkit2gtk-4.0-dev libsoup2.4-dev libgtk-3-dev" +] diff --git a/desktop/tauri/src-tauri/src/xdg/mod.rs b/desktop/tauri/src-tauri/src/xdg/mod.rs index b1fa9089..a07dd47e 100644 --- a/desktop/tauri/src-tauri/src/xdg/mod.rs +++ b/desktop/tauri/src-tauri/src/xdg/mod.rs @@ -404,7 +404,7 @@ fn get_icon_as_png_dataurl(name: &str, size: i8) -> Result<(String, String)> { let icon_info = gtk_icon_theme_lookup_icon( GTK_DEFAULT_THEME.unwrap(), - c_str.as_ptr() as *const i8, + c_str.as_ptr() as *const u8, size as c_int, 0, ); diff --git a/desktop/tauri/src-tauri/tauri.conf.json b/desktop/tauri/src-tauri/tauri.conf.json index 0fca6f4f..fbdc6578 100644 --- a/desktop/tauri/src-tauri/tauri.conf.json +++ b/desktop/tauri/src-tauri/tauri.conf.json @@ -47,8 +47,8 @@ "depends": [] }, "externalBin": [ - "binaries/portmaster-start", - "binaries/portmaster-core" + "../binaries/portmaster-start", + "../binaries/portmaster-core" ], "icon": [ "../assets/icons/pm_dark_512.png", From f003ef9a9bcd999c7b932605958b096c5e11b514 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 26 Mar 2024 15:40:46 +0100 Subject: [PATCH 07/35] Added missing .gitkeep file --- runtime/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 runtime/.gitkeep diff --git a/runtime/.gitkeep b/runtime/.gitkeep new file mode 100644 index 00000000..e69de29b From 8cbc94953354a0b0b4a577250892769e426d339e Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Mar 2024 12:51:54 +0100 Subject: [PATCH 08/35] Finish earthfile and add linux packaging assets --- Earthfile | 38 +++++++++++-------- packaging/linux/.gitkeep | 0 packaging/linux/portmaster-autostart.desktop | 9 +++++ packaging/linux/portmaster.desktop | 8 ++++ packaging/linux/portmaster.service | 40 ++++++++++++++++++++ 5 files changed, 79 insertions(+), 16 deletions(-) delete mode 100644 packaging/linux/.gitkeep create mode 100644 packaging/linux/portmaster-autostart.desktop create mode 100644 packaging/linux/portmaster.desktop create mode 100644 packaging/linux/portmaster.service diff --git a/Earthfile b/Earthfile index f2c2af91..ecf6f925 100644 --- a/Earthfile +++ b/Earthfile @@ -9,12 +9,15 @@ ARG --global outputDir = "./dist" # to GOOS, GOARCH and GOARM when building go binaries. See the +RUST_TO_GO_ARCH_STRING # helper method at the bottom of the file. + ARG --global architectures = "x86_64-unknown-linux-gnu" \ "aarch64-unknown-linux-gnu" \ - "armv7-unknown-linux-gnueabihf" \ - "arm-unknown-linux-gnueabi" \ "x86_64-pc-windows-gnu" +# Compile errors here: +# "armv7-unknown-linux-gnueabihf" \ +# "arm-unknown-linux-gnueabi" \ + # Import the earthly rust lib since it already provides some useful # build-targets and methods to initialize the rust toolchain. IMPORT github.com/earthly/lib/rust:3.0.2 AS rust @@ -215,17 +218,17 @@ angular-project: RUN ./node_modules/.bin/ng build --configuration ${configuration} --base-href ${baseHref} "${project}" - RUN zip -r "./${project}.zip" "${dist}" + RUN cwd=$(pwd) && cd "${dist}" && zip -r "${cwd}/${project}.zip" ./ SAVE ARTIFACT "./${project}.zip" AS LOCAL ${outputDir}/${project}.zip SAVE ARTIFACT "./dist" AS LOCAL ${outputDir}/${project} # Build the angular projects (portmaster-UI and tauri-builtin) in production mode angular-release: - BUILD +angular-project --project=portmaster --dist=./dist --configuration=production --baseHref=/ui/modules/portmaster + BUILD +angular-project --project=portmaster --dist=./dist --configuration=production --baseHref=/ui/modules/portmaster/ # Build the angular projects (portmaster-UI and tauri-builtin) in dev mode angular-dev: - BUILD +angular-project --project=portmaster --dist=./dist --configuration=development --baseHref=/ui/modules/portmaster + BUILD +angular-project --project=portmaster --dist=./dist --configuration=development --baseHref=/ui/modules/portmaster/ # A base target for rust to prepare the build container rust-base: @@ -325,15 +328,19 @@ tauri-src: # are preserved such that Rust's incremental compilation works correctly. COPY --keep-ts ./desktop/tauri/ . COPY assets/data ./assets + COPY packaging/linux ./../../packaging/linux COPY (+angular-project/dist/tauri-builtin --project=tauri-builtin --dist=./dist/tauri-builtin --configuration=production --baseHref="/") ./../angular/dist/tauri-builtin + WORKDIR /app/tauri/src-tauri + build-tauri: FROM +tauri-src ARG --required target - ARG output="release/[^\./]+" + ARG output = ".*/release/(([^\./]+|([^\./]+\.(dll|exe)))|bundle/.*\.(deb|msi|AppImage))" ARG bundle="none" + # if we want tauri to create the installer bundles we also need to provide all external binaries # we need to do some magic here because tauri expects the binaries to include the rust target tripple. # We already knwo that triple because it's a required argument. From that triple, we use +RUST_TO_GO_ARCH_STRING @@ -351,16 +358,15 @@ build-tauri: COPY (+build-go/output --GOOS="${GOOS}" --CMDS="portmaster-start portmaster-core" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild # Place them in the correct folder with the rust target tripple attached. - LET dest="" FOR bin IN $(ls /tmp/gobuild) - SET dest="./binaries/${bin}-${target}" - - IF [ -z "${bin##*.exe}" ] - SET dest = "./binaries/${bin%.*}-${target}.exe" - END - - RUN echo "Copying ${bin} to ${dest}" - RUN cp "/tmp/gobuild/${bin}" "${dest}" + # ${bin$.*} does not work in SET commands unfortunately so we use a shell + # snippet here: + RUN set -e ; \ + dest="./binaries/${bin}-${target}" ; \ + if [ -z "${bin##*.exe}" ]; then \ + dest="./binaries/${bin%.*}-${target}.exe" ; \ + fi ; \ + cp "/tmp/gobuild/${bin}" "${dest}" ; END # Just for debugging ... @@ -389,7 +395,7 @@ build-tauri: # The following would use the CROSS function from the earthly lib, this # DO rust+CROSS --target="${target}" - RUN ls target + SAVE ARTIFACT "target/${target}/release/" AS LOCAL "${outputDir}/tauri/${target}" tauri-release: FROM alpine:3.18 diff --git a/packaging/linux/.gitkeep b/packaging/linux/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/packaging/linux/portmaster-autostart.desktop b/packaging/linux/portmaster-autostart.desktop new file mode 100644 index 00000000..4396d9c5 --- /dev/null +++ b/packaging/linux/portmaster-autostart.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=Portmaster +GenericName=Application Firewall Notifier +Exec=/usr/bin/portmaster --with-prompts --with-notifications --background +Icon=portmaster +Terminal=false +Type=Application +Categories=System +NoDisplay=true \ No newline at end of file diff --git a/packaging/linux/portmaster.desktop b/packaging/linux/portmaster.desktop new file mode 100644 index 00000000..c21458b0 --- /dev/null +++ b/packaging/linux/portmaster.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=Portmaster +GenericName=Application Firewall +Exec={{exec}} --data=/opt/safing/portmaster --with-prompts --with-notifications +Icon={{icon}} +Terminal=false +Type=Application +Categories=System diff --git a/packaging/linux/portmaster.service b/packaging/linux/portmaster.service new file mode 100644 index 00000000..d5915e34 --- /dev/null +++ b/packaging/linux/portmaster.service @@ -0,0 +1,40 @@ +[Unit] +Description=Portmaster by Safing +Documentation=https://safing.io +Documentation=https://docs.safing.io +Before=nss-lookup.target network.target shutdown.target +After=systemd-networkd.service +Conflicts=shutdown.target +Conflicts=firewalld.service +Wants=nss-lookup.target + +[Service] +Type=simple +Restart=on-failure +RestartSec=10 +RestartPreventExitStatus=24 +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +PrivateTmp=yes +PIDFile=/var/lib/portmaster/core-lock.pid +Environment=LOGLEVEL=info +Environment=PORTMASTER_ARGS= +EnvironmentFile=-/etc/default/portmaster +ProtectSystem=true +RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6 +RestrictNamespaces=yes +ProtectHome=read-only +ProtectKernelTunables=yes +ProtectKernelLogs=yes +ProtectControlGroups=yes +PrivateDevices=yes +AmbientCapabilities=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid +CapabilityBoundingSet=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid +StateDirectory=portmaster +ExecStartPre=-/usr/bin/portmaster-start --data $STATE_DIRECTORY clean-structure +ExecStart=/usr/bin/portmaster-core --data $STATE_DIRECTORY --disable-software-updates $PORTMASTER_ARGS +ExecStartPost=-/usr/bin/portmaster-start recover-iptables + +[Install] +WantedBy=multi-user.target From 3c0a362bff2a1ca8f59b736911516ab38bd38e05 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Mar 2024 12:52:17 +0100 Subject: [PATCH 09/35] Fix angular production environment --- desktop/angular/src/environments/environment.prod.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/desktop/angular/src/environments/environment.prod.ts b/desktop/angular/src/environments/environment.prod.ts index 71b53cec..8cdffedb 100644 --- a/desktop/angular/src/environments/environment.prod.ts +++ b/desktop/angular/src/environments/environment.prod.ts @@ -1,4 +1,3 @@ -/* export const environment = new class { readonly supportHub = "https://support.safing.io" readonly production = true; @@ -11,12 +10,4 @@ export const environment = new class { const result = `ws://${window.location.host}/api/database/v1`; return result; } -} -*/ - -export const environment = { - production: false, - portAPI: "ws://127.0.0.1:817/api/database/v1", - httpAPI: "http://127.0.0.1:817/api", - supportHub: "https://support.safing.io" -}; +} \ No newline at end of file From 347e2d1982947b18e0e452cc6e13b744b3917dec Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Mar 2024 12:52:52 +0100 Subject: [PATCH 10/35] Fix race condition in tauri and window not navigating in release mode --- desktop/tauri/src-tauri/src/main.rs | 8 +++++ desktop/tauri/src-tauri/src/portmaster/mod.rs | 36 +++++++++++++------ desktop/tauri/src-tauri/src/window.rs | 4 ++- desktop/tauri/src-tauri/src/xdg/mod.rs | 4 +-- desktop/tauri/src-tauri/tauri.conf.json | 16 ++++++--- 5 files changed, 50 insertions(+), 18 deletions(-) diff --git a/desktop/tauri/src-tauri/src/main.rs b/desktop/tauri/src-tauri/src/main.rs index 2b1def20..b25f8c78 100644 --- a/desktop/tauri/src-tauri/src/main.rs +++ b/desktop/tauri/src-tauri/src/main.rs @@ -38,7 +38,13 @@ struct WsHandler { } impl portmaster::Handler for WsHandler { + fn name(&self) -> String { + "main-handler".to_string() + } + fn on_connect(&mut self, cli: portapi::client::PortAPI) -> () { + info!("connection established, creating main window"); + // we successfully connected to Portmaster. Set is_first_connect to false // so we don't show the splash-screen when we loose connection. self.is_first_connect = false; @@ -52,6 +58,8 @@ impl portmaster::Handler for WsHandler { // bootstrapping. if let Err(err) = create_main_window(&self.handle) { error!("failed to create main window: {}", err.to_string()); + } else { + debug!("created main window") } let handle = self.handle.clone(); diff --git a/desktop/tauri/src-tauri/src/portmaster/mod.rs b/desktop/tauri/src-tauri/src/portmaster/mod.rs index a5844949..aecf1874 100644 --- a/desktop/tauri/src-tauri/src/portmaster/mod.rs +++ b/desktop/tauri/src-tauri/src/portmaster/mod.rs @@ -31,7 +31,7 @@ use std::{ sync::atomic::{AtomicBool, Ordering}, }; -use log::debug; +use log::{debug, error}; use serde; use std::sync::Mutex; use tauri::{ @@ -42,6 +42,7 @@ use tauri::{ pub trait Handler { fn on_connect(&mut self, cli: PortAPI) -> (); fn on_disconnect(&mut self); + fn name(&self) -> String; } pub struct PortmasterPlugin { @@ -107,18 +108,24 @@ impl PortmasterPlugin { /// Registers a new connection handler that is called on connect /// and disconnect of the Portmaster websocket API. pub fn register_handler(&self, mut handler: impl Handler + Send + 'static) { - // register_handler can only be invoked after the plugin setup - // completed. in this case, the websocket thread is already spawned and - // we might already be connected or know that the connection failed. - // Call the respective handler method immediately now. - if let Some(api) = self.get_api() { - handler.on_connect(api); - } else { - handler.on_disconnect(); - } - if let Ok(mut handlers) = self.handlers.lock() { + // register_handler can only be invoked after the plugin setup + // completed. in this case, the websocket thread is already spawned and + // we might already be connected or know that the connection failed. + // Call the respective handler method immediately now. + if let Some(api) = self.get_api() { + debug!("already connected to Portmaster API, calling on_connect()"); + + handler.on_connect(api); + } else { + debug!("not yet connected to Portmaster API, calling on_disconnect()"); + + handler.on_disconnect(); + } + handlers.push(Box::new(handler)); + + debug!("number of registered handlers: {}", handlers.len()); } } @@ -211,6 +218,8 @@ impl PortmasterPlugin { /// Internal method to call all on_connect handlers fn on_connect(&self, api: PortAPI) { + debug!("connection to portmaster established, calling handlers"); + self.is_reachable.store(true, Ordering::Relaxed); // store the new api client. @@ -224,9 +233,14 @@ impl PortmasterPlugin { } if let Ok(mut handlers) = self.handlers.lock() { + debug!("executing handler.on_connect()"); + for handler in handlers.iter_mut() { + debug!("calling registered handler: {}", handler.name()); handler.on_connect(api.clone()); } + } else { + error!("failed to lock handlers") } } diff --git a/desktop/tauri/src-tauri/src/window.rs b/desktop/tauri/src-tauri/src/window.rs index 2059a134..af3bce97 100644 --- a/desktop/tauri/src-tauri/src/window.rs +++ b/desktop/tauri/src-tauri/src/window.rs @@ -129,7 +129,7 @@ pub fn may_navigate_to_ui(win: &mut Window, force: bool) { return; } - if force || cfg!(debug_assertions) || win.url().host_str() != Some("localhost") { + if force || cfg!(debug_assertions) || win.url().as_str() == "tauri://localhost" { #[cfg(debug_assertions)] if let Ok(target_url) = std::env::var("TAURI_PM_URL") { debug!("[tauri] navigating to {}", target_url); @@ -147,5 +147,7 @@ pub fn may_navigate_to_ui(win: &mut Window, force: bool) { #[cfg(not(debug_assertions))] win.navigate("http://localhost:817".parse().unwrap()); + } else { + error!("not navigating to user interface: current url: {}", win.url().as_str()); } } diff --git a/desktop/tauri/src-tauri/src/xdg/mod.rs b/desktop/tauri/src-tauri/src/xdg/mod.rs index a07dd47e..607f5d0e 100644 --- a/desktop/tauri/src-tauri/src/xdg/mod.rs +++ b/desktop/tauri/src-tauri/src/xdg/mod.rs @@ -7,7 +7,7 @@ use gtk_sys::{ }; use log::{debug, error}; use std::collections::HashMap; -use std::ffi::c_int; +use std::ffi::{c_char, c_int}; use std::ffi::{CStr, CString}; use std::io; use std::path::{Path, PathBuf}; @@ -404,7 +404,7 @@ fn get_icon_as_png_dataurl(name: &str, size: i8) -> Result<(String, String)> { let icon_info = gtk_icon_theme_lookup_icon( GTK_DEFAULT_THEME.unwrap(), - c_str.as_ptr() as *const u8, + c_str.as_ptr() as *const c_char, size as c_int, 0, ); diff --git a/desktop/tauri/src-tauri/tauri.conf.json b/desktop/tauri/src-tauri/tauri.conf.json index fbdc6578..d3434150 100644 --- a/desktop/tauri/src-tauri/tauri.conf.json +++ b/desktop/tauri/src-tauri/tauri.conf.json @@ -42,13 +42,21 @@ "bundle": { "active": true, "category": "Utility", - "copyright": "", + "copyright": "Safing Limited Inc", "deb": { - "depends": [] + "depends": [ + "libayatana-appindicator3" + ], + "desktopTemplate": "../../../packaging/linux/portmaster.desktop", + "files": { + "/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service", + "/etc/xdg/autostart/portmaster.service": "../../../packaging/linux/portmaster-autostart.desktop", + "/var/": "../../../packaging/linux/var" + } }, "externalBin": [ - "../binaries/portmaster-start", - "../binaries/portmaster-core" + "binaries/portmaster-start", + "binaries/portmaster-core" ], "icon": [ "../assets/icons/pm_dark_512.png", From 90535c5c86d7944c018203c83f54787f424b43ae Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Mar 2024 12:55:31 +0100 Subject: [PATCH 11/35] Add support for --allowed-clients parameter to whitelist binaries that are allowed to talk to the Portmaster API --- service/firewall/api.go | 10 ++++++++++ service/firewall/module.go | 20 +++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/service/firewall/api.go b/service/firewall/api.go index 949e168f..f5f0db0f 100644 --- a/service/firewall/api.go +++ b/service/firewall/api.go @@ -6,6 +6,7 @@ import ( "net" "net/http" "path/filepath" + "slices" "strings" "time" @@ -164,6 +165,15 @@ func authenticateAPIRequest(ctx context.Context, pktInfo *packet.Info) (retry bo default: // normal process // Check if the requesting process is in database root / updates dir. if realPath, err := filepath.EvalSymlinks(proc.Path); err == nil { + + // check if the client has been allowed by flag + if slices.Contains(allowedClients, realPath) { + log.Infof("filter: access to portmaster api allowed for configured client: %s", realPath) + return false, nil + } else if len(allowedClients) > 0 { + log.Warningf("filter: process is not in the allowed clients list: %s (list=%s)", realPath, allowedClients) + } + if strings.HasPrefix(realPath, authenticatedPath) { return false, nil } diff --git a/service/firewall/module.go b/service/firewall/module.go index 73292967..168ee7b8 100644 --- a/service/firewall/module.go +++ b/service/firewall/module.go @@ -2,7 +2,9 @@ package firewall import ( "context" + "flag" "fmt" + "path/filepath" "strings" "github.com/safing/portbase/config" @@ -16,7 +18,21 @@ import ( "github.com/safing/portmaster/spn/captain" ) -var module *modules.Module +type stringSliceFlag []string + +func (ss *stringSliceFlag) String() string { + return strings.Join(*ss, ":") +} + +func (ss *stringSliceFlag) Set(value string) error { + *ss = append(*ss, filepath.Clean(value)) + return nil +} + +var ( + module *modules.Module + allowedClients stringSliceFlag +) func init() { module = modules.Register("filter", prep, start, stop, "core", "interception", "intel", "netquery") @@ -28,6 +44,8 @@ func init() { "config:filter/", nil, ) + + flag.Var(&allowedClients, "allowed-clients", "A list of binaries that are allowed to connect to the Portmaster API") } func prep() error { From de87216d5cae091dd6b87b18e7f7a714105f477a Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Mar 2024 13:03:33 +0100 Subject: [PATCH 12/35] Fix file name in xdg/autostart --- desktop/tauri/src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/tauri/src-tauri/tauri.conf.json b/desktop/tauri/src-tauri/tauri.conf.json index d3434150..9c782d5a 100644 --- a/desktop/tauri/src-tauri/tauri.conf.json +++ b/desktop/tauri/src-tauri/tauri.conf.json @@ -50,7 +50,7 @@ "desktopTemplate": "../../../packaging/linux/portmaster.desktop", "files": { "/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service", - "/etc/xdg/autostart/portmaster.service": "../../../packaging/linux/portmaster-autostart.desktop", + "/etc/xdg/autostart/portmaster.desktop": "../../../packaging/linux/portmaster-autostart.desktop", "/var/": "../../../packaging/linux/var" } }, From 8e6a99ba14fdfaa6ae199897de59da5d75b89fd3 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Mar 2024 13:56:16 +0100 Subject: [PATCH 13/35] Fix logging in firewall api for allowed-clients --- service/firewall/api.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/service/firewall/api.go b/service/firewall/api.go index f5f0db0f..244ec2b8 100644 --- a/service/firewall/api.go +++ b/service/firewall/api.go @@ -168,10 +168,7 @@ func authenticateAPIRequest(ctx context.Context, pktInfo *packet.Info) (retry bo // check if the client has been allowed by flag if slices.Contains(allowedClients, realPath) { - log.Infof("filter: access to portmaster api allowed for configured client: %s", realPath) return false, nil - } else if len(allowedClients) > 0 { - log.Warningf("filter: process is not in the allowed clients list: %s (list=%s)", realPath, allowedClients) } if strings.HasPrefix(realPath, authenticatedPath) { From 701bb916460ac0b27ecd2d513c43f6c2b77bd4b1 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Mar 2024 14:23:19 +0100 Subject: [PATCH 14/35] Update to new portbase and add experimental support for debian postinst and postrm scripts --- desktop/tauri/src-tauri/tauri.conf.json | 4 +++- go.mod | 16 ++++++++-------- go.sum | 16 ++++++++++++++++ packaging/linux/debian/postinst | 6 ++++++ packaging/linux/debian/postrm | 1 + packaging/linux/portmaster.service | 3 ++- 6 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 packaging/linux/debian/postinst create mode 100644 packaging/linux/debian/postrm diff --git a/desktop/tauri/src-tauri/tauri.conf.json b/desktop/tauri/src-tauri/tauri.conf.json index 9c782d5a..c92731be 100644 --- a/desktop/tauri/src-tauri/tauri.conf.json +++ b/desktop/tauri/src-tauri/tauri.conf.json @@ -51,7 +51,9 @@ "files": { "/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service", "/etc/xdg/autostart/portmaster.desktop": "../../../packaging/linux/portmaster-autostart.desktop", - "/var/": "../../../packaging/linux/var" + "/var/": "../../../packaging/linux/var", + "../control/postinst": "../../../packaging/linux/debian/postinst", + "../control/postrm": "../../../packaging/linux/debian/postrm" } }, "externalBin": [ diff --git a/go.mod b/go.mod index f1b496f5..be141f45 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/r3labs/diff/v3 v3.0.1 github.com/rot256/pblind v0.0.0-20231024115251-cd3f239f28c1 github.com/safing/jess v0.3.3 - github.com/safing/portbase v0.18.9 + github.com/safing/portbase v0.19.0 github.com/safing/portmaster-android/go v0.0.0-20230830120134-3226ceac3bec github.com/shirou/gopsutil v3.21.11+incompatible github.com/spf13/cobra v1.8.0 @@ -46,9 +46,9 @@ require ( github.com/vincent-petithory/dataurl v1.0.0 golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e golang.org/x/image v0.15.0 - golang.org/x/net v0.20.0 + golang.org/x/net v0.22.0 golang.org/x/sync v0.6.0 - golang.org/x/sys v0.16.0 + golang.org/x/sys v0.18.0 gopkg.in/yaml.v3 v3.0.1 zombiezen.com/go/sqlite v1.0.0 ) @@ -66,7 +66,7 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor v1.5.1 // indirect - github.com/fxamacker/cbor/v2 v2.5.0 // indirect + github.com/fxamacker/cbor/v2 v2.6.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus v4.1.0+incompatible // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect @@ -94,7 +94,7 @@ require ( github.com/seehuhn/fortuna v1.0.1 // indirect github.com/seehuhn/sha256d v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/tidwall/gjson v1.17.0 // indirect + github.com/tidwall/gjson v1.17.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect @@ -105,17 +105,17 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/yusufpapurcu/wmi v1.2.3 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zalando/go-keyring v0.2.3 // indirect github.com/zeebo/blake3 v0.2.3 // indirect go.etcd.io/bbolt v1.3.8 // indirect - golang.org/x/crypto v0.18.0 // indirect + golang.org/x/crypto v0.21.0 // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.16.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gvisor.dev/gvisor v0.0.0-20240110202538-8053cd8f0bf6 // indirect + gvisor.dev/gvisor v0.0.0-20240327015314-08ed01b28587 // indirect modernc.org/libc v1.40.1 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.7.2 // indirect diff --git a/go.sum b/go.sum index 60ead25b..15727906 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,8 @@ github.com/fxamacker/cbor v1.5.1 h1:XjQWBgdmQyqimslUh5r4tUGmoqzHmBFQOImkWGi2awg= github.com/fxamacker/cbor v1.5.1/go.mod h1:3aPGItF174ni7dDzd6JZ206H8cmr4GDNBGpPa971zsU= github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= +github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -223,6 +225,8 @@ github.com/safing/jess v0.3.3 h1:0U0bWdO0sFCgox+nMOqISFrnJpVmi+VFOW1xdX6q3qw= github.com/safing/jess v0.3.3/go.mod h1:t63qHB+4xd1HIv9MKN/qI2rc7ytvx7d6l4hbX7zxer0= github.com/safing/portbase v0.18.9 h1:j+ToHKQz0U2+Tx4jMP7QPky/H0R4uY6qUM+lIJlO6ks= github.com/safing/portbase v0.18.9/go.mod h1:Qrh3ck+7VZloFmnozCs9Hj8godhJAi55cmiDiC7BwTc= +github.com/safing/portbase v0.19.0 h1:2T6f/w90IdIsSgUfyXoveqZM7tVwW+IFrtLbPVXtY3k= +github.com/safing/portbase v0.19.0/go.mod h1:Qrh3ck+7VZloFmnozCs9Hj8godhJAi55cmiDiC7BwTc= github.com/safing/portmaster-android/go v0.0.0-20230830120134-3226ceac3bec h1:oSJY1seobofPwpMoJRkCgXnTwfiQWNfGMCPDfqgAEfg= github.com/safing/portmaster-android/go v0.0.0-20230830120134-3226ceac3bec/go.mod h1:abwyAQrZGemWbSh/aCD9nnkp0SvFFf/mGWkAbOwPnFE= github.com/safing/spn v0.7.5 h1:WfkMs2omLrwxBWccGGG9Akx0AvsvJLG+W7rjWQpQhl4= @@ -266,6 +270,8 @@ github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K0 github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= @@ -296,6 +302,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= @@ -312,6 +320,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE= golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= @@ -346,6 +356,8 @@ golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -389,6 +401,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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= @@ -430,6 +444,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gvisor.dev/gvisor v0.0.0-20240110202538-8053cd8f0bf6 h1:Ass5FAjCCQ5WECPE9NN7ItZnKJ38i6sM8MCMNBGee5I= gvisor.dev/gvisor v0.0.0-20240110202538-8053cd8f0bf6/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk= +gvisor.dev/gvisor v0.0.0-20240327015314-08ed01b28587 h1:wH3g/qTCPlVBwkFktYuKNFJGeo7ctLNEjzrMlfPrVgE= +gvisor.dev/gvisor v0.0.0-20240327015314-08ed01b28587/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0= honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= modernc.org/libc v1.40.1 h1:ZhRylEBcj3GyQbPVC8JxIg7SdrT4JOxIDJoUon0NfF8= diff --git a/packaging/linux/debian/postinst b/packaging/linux/debian/postinst new file mode 100644 index 00000000..8f727403 --- /dev/null +++ b/packaging/linux/debian/postinst @@ -0,0 +1,6 @@ +#!/bin/bash + +systemctl daemon-reload +systemctl enable portmaster.service + +echo "Please reboot your system" \ No newline at end of file diff --git a/packaging/linux/debian/postrm b/packaging/linux/debian/postrm new file mode 100644 index 00000000..a9bf588e --- /dev/null +++ b/packaging/linux/debian/postrm @@ -0,0 +1 @@ +#!/bin/bash diff --git a/packaging/linux/portmaster.service b/packaging/linux/portmaster.service index d5915e34..81574193 100644 --- a/packaging/linux/portmaster.service +++ b/packaging/linux/portmaster.service @@ -33,7 +33,8 @@ AmbientCapabilities=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_ne CapabilityBoundingSet=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid StateDirectory=portmaster ExecStartPre=-/usr/bin/portmaster-start --data $STATE_DIRECTORY clean-structure -ExecStart=/usr/bin/portmaster-core --data $STATE_DIRECTORY --disable-software-updates $PORTMASTER_ARGS +# TODO(ppacher): add --disable-software-updates once it's merged and the release process changed. +ExecStart=/usr/bin/portmaster-core --data $STATE_DIRECTORY $PORTMASTER_ARGS ExecStartPost=-/usr/bin/portmaster-start recover-iptables [Install] From a24e60c2e582c7a433c91ad20ad00837ce65499c Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Mar 2024 14:26:41 +0100 Subject: [PATCH 15/35] Remove obsolete build, pack and test scripts --- pack | 65 -------------------- test | 193 ----------------------------------------------------------- 2 files changed, 258 deletions(-) delete mode 100755 pack delete mode 100755 test diff --git a/pack b/pack deleted file mode 100755 index e23f1286..00000000 --- a/pack +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash - -baseDir="$( cd "$(dirname "$0")" && pwd )" -cd "$baseDir" - -COL_OFF="\033[0m" -COL_BOLD="\033[01;01m" -COL_RED="\033[31m" -COL_GREEN="\033[32m" -COL_YELLOW="\033[33m" - -function safe_execute { - echo -e "\n[....] $*" - $* - if [[ $? -eq 0 ]]; then - echo -e "[${COL_GREEN} OK ${COL_OFF}] $*" - else - echo -e "[${COL_RED}FAIL${COL_OFF}] $*" >/dev/stderr - echo -e "[${COL_RED}CRIT${COL_OFF}] ABORTING..." >/dev/stderr - exit 1 - fi -} - -function check { - ./cmds/portmaster-core/pack check - ./cmds/portmaster-start/pack check - ./cmds/hub/pack check -} - -function build { - safe_execute ./cmds/portmaster-core/pack build - safe_execute ./cmds/portmaster-start/pack build - safe_execute ./cmds/hub/pack build -} - -function reset { - ./cmds/portmaster-core/pack reset - ./cmds/portmaster-start/pack reset - ./cmds/hub/pack resset -} - -case $1 in - "check" ) - check - ;; - "build" ) - build - ;; - "reset" ) - reset - ;; - * ) - echo "" - echo "build list:" - echo "" - check - echo "" - read -p "press [Enter] to start building" x - echo "" - build - echo "" - echo "finished building." - echo "" - ;; -esac diff --git a/test b/test deleted file mode 100755 index 71a65921..00000000 --- a/test +++ /dev/null @@ -1,193 +0,0 @@ -#!/bin/bash - -warnings=0 -errors=0 -scripted=0 -goUp="\\e[1A" -fullTestFlags="-short" -install=0 -testonly=0 - -function help { - echo "usage: $0 [command] [options]" - echo "" - echo "commands:" - echo " run baseline tests" - echo " full run full tests (ie. not short)" - echo " install install deps for running tests" - echo "" - echo "options:" - echo " --scripted don't jump console lines (still use colors)" - echo " --test-only run tests only, no linters" - echo " [package] run only on this package" -} - -function run { - if [[ $scripted -eq 0 ]]; then - echo "[......] $*" - fi - - # create tmpfile - tmpfile=$(mktemp) - # execute - $* >$tmpfile 2>&1 - rc=$? - output=$(cat $tmpfile) - - # check return code - if [[ $rc -eq 0 ]]; then - if [[ $output == *"[no test files]"* ]]; then - echo -e "${goUp}[\e[01;33mNOTEST\e[00m] $*" - warnings=$((warnings+1)) - else - echo -ne "${goUp}[\e[01;32m OK \e[00m] " - if [[ $2 == "test" ]]; then - echo -n $* - echo -n ": " - echo $output | cut -f "3-" -d " " - else - echo $* - fi - fi - else - if [[ $output == *"build constraints exclude all Go files"* ]]; then - echo -e "${goUp}[ !=OS ] $*" - else - echo -e "${goUp}[\e[01;31m FAIL \e[00m] $*" - cat $tmpfile - errors=$((errors+1)) - fi - fi - - rm -f $tmpfile -} - -# get and switch to script dir -baseDir="$( cd "$(dirname "$0")" && pwd )" -cd "$baseDir" - -# args -while true; do - case "$1" in - "-h"|"help"|"--help") - help - exit 0 - ;; - "--scripted") - scripted=1 - goUp="" - shift 1 - ;; - "--test-only") - testonly=1 - shift 1 - ;; - "install") - install=1 - shift 1 - ;; - "full") - fullTestFlags="" - shift 1 - ;; - *) - break - ;; - esac -done - -# check if $GOPATH/bin is in $PATH -if [[ $PATH != *"$GOPATH/bin"* ]]; then - export PATH=$GOPATH/bin:$PATH -fi - -# install -if [[ $install -eq 1 ]]; then - echo "installing dependencies..." - # TODO: update golangci-lint version regularly - echo "$ curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.44.0" - curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.44.0 - exit 0 -fi - -# check dependencies -if [[ $(which go) == "" ]]; then - echo "go command not found" - exit 1 -fi -if [[ $testonly -eq 0 ]]; then - if [[ $(which gofmt) == "" ]]; then - echo "gofmt command not found" - exit 1 - fi - if [[ $(which golangci-lint) == "" ]]; then - echo "golangci-lint command not found" - echo "install with: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin vX.Y.Z" - echo "don't forget to specify the version you want" - echo "or run: ./test install" - echo "" - echo "alternatively, install the current dev version with: go get -u github.com/golangci/golangci-lint/cmd/golangci-lint" - exit 1 - fi -fi - -# build portmaster-core for for all supported platforms -echo "building portmaster-core for all platforms" -cd ./cmds/portmaster-core -run env GOOS=linux GOARCH=amd64 ./build -run env GOOS=windows GOARCH=amd64 ./build -run env GOOS=darwin GOARCH=amd64 ./build -run env GOOS=linux GOARCH=arm64 ./build -run env GOOS=windows GOARCH=arm64 ./build -run env GOOS=darwin GOARCH=arm64 ./build -cd "$baseDir" - -# build portmaster-start for for all supported platforms -echo "" -echo "building portmaster-start for all platforms" -cd ./cmds/portmaster-start -run env GOOS=linux GOARCH=amd64 ./build -# run env GOOS=windows GOARCH=amd64 ./build # TODO: Fix for GitHub CI -run env GOOS=darwin GOARCH=amd64 ./build -run env GOOS=linux GOARCH=arm64 ./build -# run env GOOS=windows GOARCH=arm64 ./build # TODO: Fix for GitHub CI -run env GOOS=darwin GOARCH=arm64 ./build -cd "$baseDir" - -# target selection -if [[ "$1" == "" ]]; then - # get all packages - packages=$(go list -e ./...) -else - # single package testing - packages=$(go list -e)/$1 - echo "" - echo "note: only running tests for package $packages" -fi - -# platform info -echo "" -platformInfo=$(go env GOOS GOARCH) -echo "running tests for ${platformInfo//$'\n'/ }:" - -# run vet/test on packages -for package in $packages; do - packagename=${package#github.com/safing/portmaster} #TODO: could be queried with `go list .` - packagename=${packagename#/} - echo "" - echo $package - if [[ $testonly -eq 0 ]]; then - run go vet $package - run golangci-lint run $packagename - fi - run go test -cover $fullTestFlags $package -done - -echo "" -if [[ $errors -gt 0 ]]; then - echo "failed with $errors errors and $warnings warnings" - exit 1 -else - echo "succeeded with $warnings warnings" - exit 0 -fi From ba54bed5b605bf007ee811f8fb1e0fbe60a310aa Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Mar 2024 14:46:46 +0100 Subject: [PATCH 16/35] Use earthly for testing in github actions --- .github/workflows/go.yml | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 823617ea..0e6b3dad 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -40,16 +40,9 @@ jobs: name: Test runs-on: ubuntu-latest steps: - - name: Check out code - uses: actions/checkout@v3 - - - name: Setup Go - uses: actions/setup-go@v4 + - uses: earthly/actions-setup@v1 with: - go-version: '^1.21' - - - name: Get dependencies - run: go mod download - + version: v0.8.0 + - uses: actions/checkout@v4 - name: Run tests - run: ./test --test-only + run: earthly --ci +test-go From 1cec92263de5b7b676520dfe9e617217efcd007a Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Mar 2024 14:57:33 +0100 Subject: [PATCH 17/35] earthly: all git commands to fail in ci --- Earthfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Earthfile b/Earthfile index ecf6f925..12b4aeb8 100644 --- a/Earthfile +++ b/Earthfile @@ -66,11 +66,11 @@ go-base: LET version = $(git tag --points-at) IF [ "${version}" = "" ] - SET version = $(git describe --tags --abbrev=0) + SET version = $(git describe --tags --abbrev=0 || echo "dev build") END ENV VERSION="${version}" - LET source = $(git remote -v | cut -f2 | cut -d" " -f1 | head -n 1) + LET source = $( ( git remote -v | cut -f2 | cut -d" " -f1 | head -n 1 ) || echo "unknown source" ) ENV SOURCE="${source}" # updates all go dependencies and runs go mod tidy, saving go.mod and go.sum locally. From 7ca69565010c174e0de130ff073d280d06157ade Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Mar 2024 15:00:03 +0100 Subject: [PATCH 18/35] Use short tests in github CI --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 0e6b3dad..507022af 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -45,4 +45,4 @@ jobs: version: v0.8.0 - uses: actions/checkout@v4 - name: Run tests - run: earthly --ci +test-go + run: earthly --ci +test-go --TESTFLAGS="-short" From b2acbe38d2269f7b41adfe6eaaabed1706286bf2 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Mar 2024 15:04:47 +0100 Subject: [PATCH 19/35] spn: fix http info page template --- spn/ships/http_info_page.html.tmpl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spn/ships/http_info_page.html.tmpl b/spn/ships/http_info_page.html.tmpl index 5a4805ed..eafdcc47 100644 --- a/spn/ships/http_info_page.html.tmpl +++ b/spn/ships/http_info_page.html.tmpl @@ -99,9 +99,8 @@ Build:
  • Commit: {{ .Info.Commit }}
  • -
  • Host: {{ .Info.BuildHost }}
  • -
  • Date: {{ .Info.BuildDate }}
  • -
  • Source: {{ .Info.BuildSource }}
  • +
  • Date: {{ .Info.Time }}
  • +
  • Source: {{ .Info.Source }}
From 6ae80e5b816384742cd82e296e7fcd202b56f733 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Mar 2024 15:15:03 +0100 Subject: [PATCH 20/35] github-actions: try to fix linter --- .github/workflows/go.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 507022af..d8c4da70 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -31,6 +31,7 @@ jobs: with: version: v1.52.2 only-new-issues: true + skip-go-installation: true args: -c ./.golangci.yml --timeout 15m - name: Run go vet From 9c226d9c542bfd96e482fbedba0030379166c8a7 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Mar 2024 15:17:38 +0100 Subject: [PATCH 21/35] Update golangci-lint-action to v4 --- .github/workflows/go.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index d8c4da70..c5cff593 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -5,6 +5,7 @@ on: branches: - master - develop + pull_request: branches: - master @@ -27,11 +28,10 @@ jobs: run: go mod download - name: Run golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v4 with: version: v1.52.2 only-new-issues: true - skip-go-installation: true args: -c ./.golangci.yml --timeout 15m - name: Run go vet From 352d6251586cf267609f03b11a9da3d69d50a507 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Mar 2024 15:20:00 +0100 Subject: [PATCH 22/35] github-actions: try to fix linter --- .github/workflows/go.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c5cff593..33673afc 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,6 +2,8 @@ name: Go on: push: + paths: + - '**.go' branches: - master - develop @@ -20,12 +22,10 @@ jobs: uses: actions/checkout@v3 - name: Setup Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: '^1.21' - - - name: Get dependencies - run: go mod download + cache: false - name: Run golangci-lint uses: golangci/golangci-lint-action@v4 From 653a365bce14b414cdb31d4cea7980798eecd623 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Mar 2024 15:29:38 +0100 Subject: [PATCH 23/35] github-actions: increase golangci-lint version since it fails with go1.21 --- .github/workflows/go.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 33673afc..8362ac6d 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 @@ -30,7 +30,7 @@ jobs: - name: Run golangci-lint uses: golangci/golangci-lint-action@v4 with: - version: v1.52.2 + version: v1.57.1 only-new-issues: true args: -c ./.golangci.yml --timeout 15m From 61176af14eff830a2c23b1a3cb96180b6f631804 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Mar 2024 16:17:58 +0100 Subject: [PATCH 24/35] Fix linting errors --- .golangci.yml | 3 +++ assets/icons_default.go | 5 ++-- cmds/notifier/http_api.go | 18 +++++++------- cmds/notifier/icons.go | 2 +- cmds/notifier/main.go | 2 ++ cmds/notifier/notification.go | 7 +++--- cmds/notifier/notify.go | 1 - cmds/notifier/notify_linux.go | 12 +++++++--- cmds/notifier/spn.go | 7 +++--- cmds/notifier/subsystems.go | 5 ++-- cmds/notifier/tray.go | 3 +-- cmds/portmaster-start/lock.go | 2 +- service/core/api.go | 11 +++++---- .../firewall/interception/nfq/conntrack.go | 5 ++-- service/firewall/packet_handler.go | 12 ---------- service/intel/entity.go | 4 ++-- service/intel/filterlists/decoder.go | 14 ++++++----- service/intel/geoip/lookup.go | 4 ++-- service/nameserver/module.go | 4 +--- service/nameserver/nameserver.go | 16 +++++++------ service/netquery/active_chart_handler.go | 4 ++-- service/netquery/bandwidth_chart_handler.go | 4 ++-- service/netquery/manager.go | 8 +++---- service/netquery/orm/decoder.go | 18 +++++++------- service/netquery/orm/encoder.go | 3 ++- service/netquery/orm/encoder_test.go | 5 ++-- service/netquery/orm/schema_builder.go | 2 +- service/netquery/orm/schema_builder_test.go | 3 ++- service/netquery/query_handler.go | 6 +++-- service/netquery/query_test.go | 4 ++-- service/network/api.go | 8 +++---- service/network/connection.go | 2 ++ service/network/packet/packet.go | 13 +++++----- service/network/socket/socket.go | 2 +- service/process/api.go | 3 +-- service/process/database.go | 3 ++- service/status/module.go | 2 +- service/updates/helper/indexes.go | 10 ++++---- service/updates/main.go | 4 ++-- spn/access/api.go | 5 ++-- spn/captain/navigation.go | 4 ++-- spn/crew/connect.go | 4 ++-- spn/hub/format_test.go | 24 +++++++++---------- spn/hub/transport_test.go | 9 +++---- spn/navigator/optimize_region.go | 4 ++-- spn/navigator/update.go | 6 ++--- spn/ships/http_shared_test.go | 21 ++++++++-------- spn/sluice/sluice.go | 2 +- 48 files changed, 167 insertions(+), 153 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 9893ff74..d6892cbc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -38,6 +38,9 @@ linters: - whitespace - wrapcheck - wsl + - perfsprint # TODO(ppacher): we should re-enanble this one to avoid costly fmt.* calls in the hot-path + - testifylint + - gomoddirectives linters-settings: revive: diff --git a/assets/icons_default.go b/assets/icons_default.go index 2c1b6eb1..2530f309 100644 --- a/assets/icons_default.go +++ b/assets/icons_default.go @@ -9,8 +9,9 @@ import ( "image" "image/png" - "github.com/safing/portbase/log" "golang.org/x/image/draw" + + "github.com/safing/portbase/log" ) // Colored Icon IDs. @@ -35,7 +36,7 @@ var ( //go:embed data/icons/pm_light_blue_512.png BluePNG []byte - // ColoredIcons holds all the icons as .PNGs + // ColoredIcons holds all the icons as .PNGs. ColoredIcons [4][]byte ) diff --git a/cmds/notifier/http_api.go b/cmds/notifier/http_api.go index bb0eebf3..356b81cd 100644 --- a/cmds/notifier/http_api.go +++ b/cmds/notifier/http_api.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "io/ioutil" + "io" "net/http" "net/http/cookiejar" "strings" @@ -16,9 +16,7 @@ const ( apiShutdownEndpoint = "core/shutdown" ) -var ( - httpApiClient *http.Client -) +var httpAPIClient *http.Client func init() { // Make cookie jar. @@ -29,22 +27,22 @@ func init() { } // Create client. - httpApiClient = &http.Client{ + httpAPIClient = &http.Client{ Jar: jar, Timeout: 3 * time.Second, } } -func httpApiAction(endpoint string) (response string, err error) { +func httpAPIAction(endpoint string) (response string, err error) { // Make action request. - resp, err := httpApiClient.Post(apiBaseURL+endpoint, "", nil) + resp, err := httpAPIClient.Post(apiBaseURL+endpoint, "", nil) if err != nil { return "", fmt.Errorf("request failed: %w", err) } // Read the response body. - defer resp.Body.Close() - respData, err := ioutil.ReadAll(resp.Body) + defer func() { _ = resp.Body.Close() }() + respData, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("failed to read data: %w", err) } @@ -60,6 +58,6 @@ func httpApiAction(endpoint string) (response string, err error) { // TriggerShutdown triggers a shutdown via the APi. func TriggerShutdown() error { - _, err := httpApiAction(apiShutdownEndpoint) + _, err := httpAPIAction(apiShutdownEndpoint) return err } diff --git a/cmds/notifier/icons.go b/cmds/notifier/icons.go index 93b3db74..b3690a3f 100644 --- a/cmds/notifier/icons.go +++ b/cmds/notifier/icons.go @@ -18,7 +18,7 @@ func ensureAppIcon() (location string, err error) { if appIconPath == "" { appIconPath = filepath.Join(dataDir, "exec", "portmaster.png") } - err = os.WriteFile(appIconPath, icons.PNG, 0o0644) + err = os.WriteFile(appIconPath, icons.PNG, 0o0644) // nolint:gosec }) return appIconPath, err diff --git a/cmds/notifier/main.go b/cmds/notifier/main.go index 8bd95b3b..4cc9f84c 100644 --- a/cmds/notifier/main.go +++ b/cmds/notifier/main.go @@ -52,6 +52,8 @@ var ( } ) +const query = "query " + func init() { flag.StringVar(&dataDir, "data", "", "set data directory") flag.BoolVar(&printStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down") diff --git a/cmds/notifier/notification.go b/cmds/notifier/notification.go index fc37690b..075dba83 100644 --- a/cmds/notifier/notification.go +++ b/cmds/notifier/notification.go @@ -14,7 +14,7 @@ type Notification struct { systemID NotificationID } -// IsSupported returns whether the action is supported on this system. +// IsSupportedAction returns whether the action is supported on this system. func IsSupportedAction(a pbnotify.Action) bool { switch a.Type { case pbnotify.ActionTypeNone: @@ -26,11 +26,10 @@ func IsSupportedAction(a pbnotify.Action) bool { // SelectAction sends an action back to the portmaster. func (n *Notification) SelectAction(action string) { - new := &pbnotify.Notification{ + upd := &pbnotify.Notification{ EventID: n.EventID, SelectedActionID: action, } - // FIXME: check response - apiClient.Update(fmt.Sprintf("%s%s", dbNotifBasePath, new.EventID), new, nil) + _ = apiClient.Update(fmt.Sprintf("%s%s", dbNotifBasePath, upd.EventID), upd, nil) } diff --git a/cmds/notifier/notify.go b/cmds/notifier/notify.go index 1b271b67..2286dff6 100644 --- a/cmds/notifier/notify.go +++ b/cmds/notifier/notify.go @@ -9,7 +9,6 @@ import ( "github.com/safing/portbase/api/client" "github.com/safing/portbase/formats/dsd" "github.com/safing/portbase/log" - pbnotify "github.com/safing/portbase/notifications" ) diff --git a/cmds/notifier/notify_linux.go b/cmds/notifier/notify_linux.go index bcf650cf..ba3f638e 100644 --- a/cmds/notifier/notify_linux.go +++ b/cmds/notifier/notify_linux.go @@ -2,9 +2,11 @@ package main import ( "context" + "errors" "sync" notify "github.com/dhaavi/go-notify" + "github.com/safing/portbase/log" ) @@ -45,7 +47,12 @@ listenForNotifications: continue listenForNotifications } - notification := n.(*Notification) + notification, ok := n.(*Notification) + if !ok { + log.Errorf("received invalid notification type %T", n) + + continue listenForNotifications + } log.Tracef("notify: received signal: %+v", sig) if sig.ActionKey != "" { @@ -62,7 +69,6 @@ listenForNotifications: } } } - } func actionListener() { @@ -71,7 +77,7 @@ func actionListener() { go handleActions(mainCtx, actions) err := notify.SignalNotify(mainCtx, actions) - if err != nil && err != context.Canceled { + if err != nil && errors.Is(err, context.Canceled) { log.Errorf("notify: signal listener failed: %s", err) } } diff --git a/cmds/notifier/spn.go b/cmds/notifier/spn.go index 1da5639d..d313716b 100644 --- a/cmds/notifier/spn.go +++ b/cmds/notifier/spn.go @@ -4,10 +4,11 @@ import ( "sync" "time" + "github.com/tevino/abool" + "github.com/safing/portbase/api/client" "github.com/safing/portbase/formats/dsd" "github.com/safing/portbase/log" - "github.com/tevino/abool" ) const ( @@ -48,10 +49,10 @@ func updateSPNStatus(s *SPNStatus) { } func spnStatusClient() { - moduleQueryOp := apiClient.Qsub("query "+spnModuleKey, handleSPNModuleUpdate) + moduleQueryOp := apiClient.Qsub(query+spnModuleKey, handleSPNModuleUpdate) moduleQueryOp.EnableResuscitation() - statusQueryOp := apiClient.Qsub("query "+spnStatusKey, handleSPNStatusUpdate) + statusQueryOp := apiClient.Qsub(query+spnStatusKey, handleSPNStatusUpdate) statusQueryOp.EnableResuscitation() } diff --git a/cmds/notifier/subsystems.go b/cmds/notifier/subsystems.go index f810538c..8444bf13 100644 --- a/cmds/notifier/subsystems.go +++ b/cmds/notifier/subsystems.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "sync" "github.com/safing/portbase/api/client" @@ -14,7 +13,7 @@ const ( // Module Failure Status Values // FailureNone = 0 // unused - // FailureHint = 1 // unused + // FailureHint = 1 // unused. FailureWarning = 2 FailureError = 3 ) @@ -92,7 +91,7 @@ func clearSubsystems() { } func subsystemsClient() { - subsystemsOp := apiClient.Qsub(fmt.Sprintf("query %s", subsystemsKeySpace), handleSubsystem) + subsystemsOp := apiClient.Qsub("query "+subsystemsKeySpace, handleSubsystem) subsystemsOp.EnableResuscitation() } diff --git a/cmds/notifier/tray.go b/cmds/notifier/tray.go index 5766611f..4044d4f7 100644 --- a/cmds/notifier/tray.go +++ b/cmds/notifier/tray.go @@ -102,7 +102,6 @@ func onReady() { } func onExit() { - } func triggerTrayUpdate() { @@ -172,7 +171,7 @@ func updateTray() { // Set SPN status if changed. if spnStatus != nil && activeSPNStatus != spnStatus.Status { activeSPNStatus = spnStatus.Status - menuItemSPNStatus.SetTitle("SPN: " + strings.Title(activeSPNStatus)) + menuItemSPNStatus.SetTitle("SPN: " + strings.Title(activeSPNStatus)) // nolint:staticcheck } // Set SPN switch if changed. diff --git a/cmds/portmaster-start/lock.go b/cmds/portmaster-start/lock.go index 0db86606..0526084c 100644 --- a/cmds/portmaster-start/lock.go +++ b/cmds/portmaster-start/lock.go @@ -79,7 +79,7 @@ func createInstanceLock(lockFilePath string) error { // create lock file // TODO: Investigate required permissions. - err = os.WriteFile(lockFilePath, []byte(fmt.Sprintf("%d", os.Getpid())), 0o0666) //nolint:gosec + err = os.WriteFile(lockFilePath, []byte(strconv.Itoa(os.Getpid())), 0o0666) //nolint:gosec if err != nil { return err } diff --git a/service/core/api.go b/service/core/api.go index 8e7d24bc..abc43dad 100644 --- a/service/core/api.go +++ b/service/core/api.go @@ -3,6 +3,7 @@ package core import ( "context" "encoding/hex" + "errors" "fmt" "net/http" "net/url" @@ -23,6 +24,8 @@ import ( "github.com/safing/portmaster/spn/captain" ) +var errInvalidReadPermission = errors.New("invalid read permission") + func registerAPIEndpoints() error { if err := api.RegisterEndpoint(api.Endpoint{ Path: "core/shutdown", @@ -207,10 +210,10 @@ func authorizeApp(ar *api.Request) (interface{}, error) { // convert the requested read and write permissions to their api.Permission // value. This ensures only "user" or "admin" permissions can be requested. if getSavePermission(readPermStr) <= api.NotSupported { - return nil, fmt.Errorf("invalid read permission") + return nil, errInvalidReadPermission } if getSavePermission(writePermStr) <= api.NotSupported { - return nil, fmt.Errorf("invalid read permission") + return nil, errInvalidReadPermission } proc, err := process.GetProcessByRequestOrigin(ar) @@ -281,7 +284,7 @@ func authorizeApp(ar *api.Request) (interface{}, error) { select { case key := <-ch: if len(key) == 0 { - return nil, fmt.Errorf("access denied") + return nil, errors.New("access denied") } return map[string]interface{}{ @@ -289,6 +292,6 @@ func authorizeApp(ar *api.Request) (interface{}, error) { "validUntil": validUntil, }, nil case <-ar.Context().Done(): - return nil, fmt.Errorf("timeout") + return nil, errors.New("timeout") } } diff --git a/service/firewall/interception/nfq/conntrack.go b/service/firewall/interception/nfq/conntrack.go index 6959d328..ea7761e4 100644 --- a/service/firewall/interception/nfq/conntrack.go +++ b/service/firewall/interception/nfq/conntrack.go @@ -4,6 +4,7 @@ package nfq import ( "encoding/binary" + "errors" "fmt" ct "github.com/florianl/go-conntrack" @@ -35,7 +36,7 @@ func TeardownNFCT() { // DeleteAllMarkedConnection deletes all marked entries from the conntrack table. func DeleteAllMarkedConnection() error { if nfct == nil { - return fmt.Errorf("nfq: nfct not initialized") + return errors.New("nfq: nfct not initialized") } // Delete all ipv4 marked connections @@ -87,7 +88,7 @@ func deleteMarkedConnections(nfct *ct.Nfct, f ct.Family) (deleted int) { // DeleteMarkedConnection removes a specific connection from the conntrack table. func DeleteMarkedConnection(conn *network.Connection) error { if nfct == nil { - return fmt.Errorf("nfq: nfct not initialized") + return errors.New("nfq: nfct not initialized") } con := ct.Con{ diff --git a/service/firewall/packet_handler.go b/service/firewall/packet_handler.go index 22d9ce37..d4b3bbe2 100644 --- a/service/firewall/packet_handler.go +++ b/service/firewall/packet_handler.go @@ -612,18 +612,6 @@ func issueVerdict(conn *network.Connection, pkt packet.Packet, verdict network.V } } -// verdictRating rates the privacy and security aspect of verdicts from worst to best. -var verdictRating = []network.Verdict{ - network.VerdictAccept, // Connection allowed in the open. - network.VerdictRerouteToTunnel, // Connection allowed, but protected. - network.VerdictRerouteToNameserver, // Connection allowed, but resolved via Portmaster. - network.VerdictBlock, // Connection blocked, with feedback. - network.VerdictDrop, // Connection blocked, without feedback. - network.VerdictFailed, - network.VerdictUndeterminable, - network.VerdictUndecided, -} - // func tunnelHandler(pkt packet.Packet) { // tunnelInfo := GetTunnelInfo(pkt.Info().Dst) // if tunnelInfo == nil { diff --git a/service/intel/entity.go b/service/intel/entity.go index 5311881a..df67edfc 100644 --- a/service/intel/entity.go +++ b/service/intel/entity.go @@ -2,9 +2,9 @@ package intel import ( "context" - "fmt" "net" "sort" + "strconv" "strings" "sync" @@ -433,7 +433,7 @@ func (e *Entity) getASNLists(ctx context.Context) { } e.loadAsnListOnce.Do(func() { - asnStr := fmt.Sprintf("%d", asn) + asnStr := strconv.FormatUint(uint64(asn), 10) list, err := filterlists.LookupASNString(asnStr) if err != nil { log.Tracer(ctx).Errorf("intel: failed to get ASN blocklist for %d: %s", asn, err) diff --git a/service/intel/filterlists/decoder.go b/service/intel/filterlists/decoder.go index b8837aca..e66d8d84 100644 --- a/service/intel/filterlists/decoder.go +++ b/service/intel/filterlists/decoder.go @@ -103,18 +103,19 @@ func parseHeader(r io.Reader) (compressed bool, format byte, err error) { if _, err = r.Read(listHeader[:]); err != nil { // if we have an error here we can safely abort because // the file must be broken - return + return compressed, format, err } if listHeader[0] != dsd.LIST { err = fmt.Errorf("unexpected file type: %d (%c), expected dsd list", listHeader[0], listHeader[0]) - return + + return compressed, format, err } var compression [1]byte if _, err = r.Read(compression[:]); err != nil { // same here, a DSDL file must have at least 2 bytes header - return + return compressed, format, err } if compression[0] == dsd.GZIP { @@ -122,15 +123,16 @@ func parseHeader(r io.Reader) (compressed bool, format byte, err error) { var formatSlice [1]byte if _, err = r.Read(formatSlice[:]); err != nil { - return + return compressed, format, err } format = formatSlice[0] - return + return compressed, format, err } format = compression[0] - return // nolint:nakedret + + return compressed, format, err } // byteReader extends an io.Reader to implement the ByteReader interface. diff --git a/service/intel/geoip/lookup.go b/service/intel/geoip/lookup.go index da69fa26..0aeef434 100644 --- a/service/intel/geoip/lookup.go +++ b/service/intel/geoip/lookup.go @@ -1,7 +1,7 @@ package geoip import ( - "fmt" + "errors" "net" "github.com/oschwald/maxminddb-golang" @@ -16,7 +16,7 @@ func getReader(ip net.IP) *maxminddb.Reader { func GetLocation(ip net.IP) (*Location, error) { db := getReader(ip) if db == nil { - return nil, fmt.Errorf("geoip database not available") + return nil, errors.New("geoip database not available") } record := &Location{} if err := db.Lookup(ip, record); err != nil { diff --git a/service/nameserver/module.go b/service/nameserver/module.go index 287ba48e..8380583b 100644 --- a/service/nameserver/module.go +++ b/service/nameserver/module.go @@ -191,10 +191,8 @@ func handleListenError(err error, ip net.IP, port uint16, primaryListener bool) EventID: eventIDConflictingService + secondaryEventIDSuffix, Type: notifications.Error, Title: "Conflicting DNS Software", - Message: fmt.Sprintf( - "Restart Portmaster after you have deactivated or properly configured the conflicting software: %s", + Message: "Restart Portmaster after you have deactivated or properly configured the conflicting software: " + cfDescription, - ), ShowOnSystem: true, AvailableActions: []*notifications.Action{ { diff --git a/service/nameserver/nameserver.go b/service/nameserver/nameserver.go index 55195756..66bccd8e 100644 --- a/service/nameserver/nameserver.go +++ b/service/nameserver/nameserver.go @@ -21,6 +21,8 @@ import ( var hostname string +const internalError = "internal error: " + func handleRequestAsWorker(w dns.ResponseWriter, query *dns.Msg) { err := module.RunWorker("handle dns request", func(ctx context.Context) error { return handleRequest(ctx, w, query) @@ -130,7 +132,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) tracer.Tracef("nameserver: delaying failing lookup until end of fail duration for %s", remainingFailingDuration.Round(time.Millisecond)) time.Sleep(remainingFailingDuration) return reply(nsutil.ServerFailure( - "internal error: "+failingErr.Error(), + internalError+failingErr.Error(), "delayed failing query to mitigate request flooding", )) } @@ -138,7 +140,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) tracer.Tracef("nameserver: delaying failing lookup for %s", failingDelay.Round(time.Millisecond)) time.Sleep(failingDelay) return reply(nsutil.ServerFailure( - "internal error: "+failingErr.Error(), + internalError+failingErr.Error(), "delayed failing query to mitigate request flooding", fmt.Sprintf("error is cached for another %s", remainingFailingDuration.Round(time.Millisecond)), )) @@ -148,7 +150,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) local, err := netenv.IsMyIP(remoteAddr.IP) if err != nil { tracer.Warningf("nameserver: failed to check if request for %s is local: %s", q.ID(), err) - return reply(nsutil.ServerFailure("internal error: failed to check if request is local")) + return reply(nsutil.ServerFailure(internalError + " failed to check if request is local")) } // Create connection ID for dns request. @@ -170,7 +172,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) conn, err = network.NewConnectionFromExternalDNSRequest(ctx, q.FQDN, nil, connID, remoteAddr.IP) if err != nil { tracer.Warningf("nameserver: failed to get host/profile for request for %s%s: %s", q.FQDN, q.QType, err) - return reply(nsutil.ServerFailure("internal error: failed to get profile")) + return reply(nsutil.ServerFailure(internalError + "failed to get profile")) } default: @@ -210,7 +212,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) case network.VerdictUndecided, network.VerdictAccept: // Check if we have a response. if rrCache == nil { - conn.Failed("internal error: no reply", "") + conn.Failed(internalError+"no reply", "") return } @@ -293,7 +295,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) tracer.Warningf("nameserver: failed to resolve %s: %s", q.ID(), err) conn.Failed(fmt.Sprintf("query failed: %s", err), "") addFailingQuery(q, err) - return reply(nsutil.ServerFailure("internal error: " + err.Error())) + return reply(nsutil.ServerFailure(internalError + err.Error())) } } // Handle special cases. @@ -301,7 +303,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) case rrCache == nil: tracer.Warning("nameserver: received successful, but empty reply from resolver") addFailingQuery(q, errors.New("emptry reply from resolver")) - return reply(nsutil.ServerFailure("internal error: empty reply")) + return reply(nsutil.ServerFailure(internalError + "empty reply")) case rrCache.RCode == dns.RcodeNameError: // Try alternatives domain names for unofficial domain spaces. altRRCache := checkAlternativeCaches(ctx, q) diff --git a/service/netquery/active_chart_handler.go b/service/netquery/active_chart_handler.go index 2d2fb682..264c903e 100644 --- a/service/netquery/active_chart_handler.go +++ b/service/netquery/active_chart_handler.go @@ -42,7 +42,7 @@ func (ch *ActiveChartHandler) ServeHTTP(resp http.ResponseWriter, req *http.Requ orm.WithResult(&result), orm.WithSchema(*ch.Database.Schema), ); err != nil { - http.Error(resp, "Failed to execute query: "+err.Error(), http.StatusInternalServerError) + http.Error(resp, failedQuery+err.Error(), http.StatusInternalServerError) return } @@ -77,7 +77,7 @@ func (ch *ActiveChartHandler) parseRequest(req *http.Request) (*QueryActiveConne var requestPayload QueryActiveConnectionChartPayload blob, err := io.ReadAll(body) if err != nil { - return nil, fmt.Errorf("failed to read body" + err.Error()) + return nil, fmt.Errorf("failed to read body: %w", err) } body = bytes.NewReader(blob) diff --git a/service/netquery/bandwidth_chart_handler.go b/service/netquery/bandwidth_chart_handler.go index 615682e6..8e5647c4 100644 --- a/service/netquery/bandwidth_chart_handler.go +++ b/service/netquery/bandwidth_chart_handler.go @@ -49,7 +49,7 @@ func (ch *BandwidthChartHandler) ServeHTTP(resp http.ResponseWriter, req *http.R orm.WithResult(&result), orm.WithSchema(*ch.Database.Schema), ); err != nil { - http.Error(resp, "Failed to execute query: "+err.Error(), http.StatusInternalServerError) + http.Error(resp, failedQuery+err.Error(), http.StatusInternalServerError) return } @@ -84,7 +84,7 @@ func (ch *BandwidthChartHandler) parseRequest(req *http.Request) (*BandwidthChar var requestPayload BandwidthChartRequest blob, err := io.ReadAll(body) if err != nil { - return nil, fmt.Errorf("failed to read body" + err.Error()) + return nil, fmt.Errorf("failed to read body: %w", err) } body = bytes.NewReader(blob) diff --git a/service/netquery/manager.go b/service/netquery/manager.go index 76403e03..d1809116 100644 --- a/service/netquery/manager.go +++ b/service/netquery/manager.go @@ -23,18 +23,18 @@ type ( // insert or an update. // The ID of Conn is unique and can be trusted to never collide with other // connections of the save device. - Save(context.Context, Conn, bool) error + Save(ctx context.Context, conn Conn, history bool) error // MarkAllHistoryConnectionsEnded marks all active connections in the history // database as ended NOW. - MarkAllHistoryConnectionsEnded(context.Context) error + MarkAllHistoryConnectionsEnded(ctx context.Context) error // RemoveAllHistoryData removes all connections from the history database. - RemoveAllHistoryData(context.Context) error + RemoveAllHistoryData(ctx context.Context) error // RemoveHistoryForProfile removes all connections from the history database. // for a given profile ID (source/id) - RemoveHistoryForProfile(context.Context, string) error + RemoveHistoryForProfile(ctx context.Context, profile string) error // UpdateBandwidth updates bandwidth data for the connection and optionally also writes // the bandwidth data to the history database. diff --git a/service/netquery/orm/decoder.go b/service/netquery/orm/decoder.go index 21ce6146..169c5e7e 100644 --- a/service/netquery/orm/decoder.go +++ b/service/netquery/orm/decoder.go @@ -41,13 +41,13 @@ type ( // by *sqlite.Stmt. Stmt interface { ColumnCount() int - ColumnName(int) string - ColumnType(int) sqlite.ColumnType - ColumnText(int) string - ColumnBool(int) bool - ColumnFloat(int) float64 - ColumnInt(int) int - ColumnReader(int) *bytes.Reader + ColumnName(col int) string + ColumnType(col int) sqlite.ColumnType + ColumnText(col int) string + ColumnBool(col int) bool + ColumnFloat(col int) float64 + ColumnInt(col int) int + ColumnReader(col int) *bytes.Reader } // DecodeFunc is called for each non-basic type during decoding. @@ -230,7 +230,7 @@ func DatetimeDecoder(loc *time.Location) DecodeFunc { case sqlite.TypeFloat: // stored as Julian day numbers - return nil, false, fmt.Errorf("REAL storage type not support for time.Time") + return nil, false, errors.New("REAL storage type not support for time.Time") case sqlite.TypeNull: return nil, true, nil @@ -359,7 +359,7 @@ func decodeBasic() DecodeFunc { case reflect.Slice: if outval.Type().Elem().Kind() != reflect.Uint8 { - return nil, false, fmt.Errorf("slices other than []byte for BLOB are not supported") + return nil, false, errors.New("slices other than []byte for BLOB are not supported") } if colType != sqlite.TypeBlob { diff --git a/service/netquery/orm/encoder.go b/service/netquery/orm/encoder.go index 6dcd0e68..8aa53387 100644 --- a/service/netquery/orm/encoder.go +++ b/service/netquery/orm/encoder.go @@ -2,6 +2,7 @@ package orm import ( "context" + "errors" "fmt" "reflect" "time" @@ -171,7 +172,7 @@ func DatetimeEncoder(loc *time.Location) EncodeFunc { valInterface := val.Interface() t, ok = valInterface.(time.Time) if !ok { - return nil, false, fmt.Errorf("cannot convert reflect value to time.Time") + return nil, false, errors.New("cannot convert reflect value to time.Time") } case valType.Kind() == reflect.String && colDef.IsTime: diff --git a/service/netquery/orm/encoder_test.go b/service/netquery/orm/encoder_test.go index d0d3c039..3a1dbb5b 100644 --- a/service/netquery/orm/encoder_test.go +++ b/service/netquery/orm/encoder_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "zombiezen.com/go/sqlite" ) @@ -120,7 +121,7 @@ func TestEncodeAsMap(t *testing.T) { //nolint:tparallel c := cases[idx] t.Run(c.Desc, func(t *testing.T) { res, err := ToParamMap(ctx, c.Input, "", DefaultEncodeConfig, nil) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, c.Expected, res) }) } @@ -253,7 +254,7 @@ func TestEncodeValue(t *testing.T) { //nolint:tparallel c := cases[idx] t.Run(c.Desc, func(t *testing.T) { res, err := EncodeValue(ctx, &c.Column, c.Input, DefaultEncodeConfig) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, c.Output, res) }) } diff --git a/service/netquery/orm/schema_builder.go b/service/netquery/orm/schema_builder.go index 018a55e1..893dab2e 100644 --- a/service/netquery/orm/schema_builder.go +++ b/service/netquery/orm/schema_builder.go @@ -274,7 +274,7 @@ func applyStructFieldTag(fieldType reflect.StructField, def *ColumnDef) error { case sqlite.TypeText: def.Default = defaultValue case sqlite.TypeBlob: - return fmt.Errorf("default values for TypeBlob not yet supported") + return errors.New("default values for TypeBlob not yet supported") default: return fmt.Errorf("failed to apply default value for unknown sqlite column type %s", def.Type) } diff --git a/service/netquery/orm/schema_builder_test.go b/service/netquery/orm/schema_builder_test.go index fdd43ec7..b39fac72 100644 --- a/service/netquery/orm/schema_builder_test.go +++ b/service/netquery/orm/schema_builder_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestSchemaBuilder(t *testing.T) { @@ -37,7 +38,7 @@ func TestSchemaBuilder(t *testing.T) { c := cases[idx] res, err := GenerateTableSchema(c.Name, c.Model) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, c.ExpectedSQL, res.CreateStatement("main", false)) } } diff --git a/service/netquery/query_handler.go b/service/netquery/query_handler.go index 68b1feb2..e996c183 100644 --- a/service/netquery/query_handler.go +++ b/service/netquery/query_handler.go @@ -19,6 +19,8 @@ import ( var charOnlyRegexp = regexp.MustCompile("[a-zA-Z]+") +const failedQuery = "Failed to execute query: " + type ( // QueryHandler implements http.Handler and allows to perform SQL @@ -78,7 +80,7 @@ func (qh *QueryHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { orm.WithResult(&result), orm.WithSchema(*qh.Database.Schema), ); err != nil { - http.Error(resp, "Failed to execute query: "+err.Error(), http.StatusInternalServerError) + http.Error(resp, failedQuery+err.Error(), http.StatusInternalServerError) return } @@ -230,7 +232,7 @@ func parseQueryRequestPayload[T any](req *http.Request) (*T, error) { //nolint:d blob, err := io.ReadAll(body) if err != nil { - return nil, fmt.Errorf("failed to read body" + err.Error()) + return nil, fmt.Errorf("failed to read body: %w", err) } body = bytes.NewReader(blob) diff --git a/service/netquery/query_test.go b/service/netquery/query_test.go index bc9fde27..0582aacf 100644 --- a/service/netquery/query_test.go +++ b/service/netquery/query_test.go @@ -102,7 +102,7 @@ func TestUnmarshalQuery(t *testing.T) { //nolint:tparallel assert.Equal(t, c.Error.Error(), err.Error()) } } else { - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, c.Expected, q) } }) @@ -241,7 +241,7 @@ func TestQueryBuilder(t *testing.T) { //nolint:tparallel assert.Equal(t, c.E.Error(), err.Error(), "test case %d", cID) } } else { - assert.NoError(t, err, "test case %d", cID) + require.NoError(t, err, "test case %d", cID) assert.Equal(t, c.P, params, "test case %d", cID) assert.Equal(t, c.R, str, "test case %d", cID) } diff --git a/service/network/api.go b/service/network/api.go index afb2d610..878db313 100644 --- a/service/network/api.go +++ b/service/network/api.go @@ -136,11 +136,11 @@ func AddNetworkDebugData(di *debug.Info, profile, where string) { // Collect matching connections. var ( //nolint:prealloc // We don't know the size. - debugConns []*Connection - accepted int - total int + debugConns []*Connection + accepted int + total int ) - + for maybeConn := range it.Next { // Switch to correct type. conn, ok := maybeConn.(*Connection) diff --git a/service/network/connection.go b/service/network/connection.go index 32ba8ee9..2459ea14 100644 --- a/service/network/connection.go +++ b/service/network/connection.go @@ -751,12 +751,14 @@ func (conn *Connection) SaveWhenFinished() { func (conn *Connection) Save() { conn.UpdateMeta() + // nolint:exhaustive switch conn.Verdict { case VerdictAccept, VerdictRerouteToNameserver: conn.ConnectionEstablished = true case VerdictRerouteToTunnel: // this is already handled when the connection tunnel has been // established. + default: } // Do not save/update until data is complete. diff --git a/service/network/packet/packet.go b/service/network/packet/packet.go index 1ac3047f..18aa7eb2 100644 --- a/service/network/packet/packet.go +++ b/service/network/packet/packet.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "strconv" "github.com/google/gopacket" ) @@ -207,9 +208,9 @@ func (pkt *Base) FmtRemoteIP() string { func (pkt *Base) FmtRemotePort() string { if pkt.info.SrcPort != 0 { if pkt.info.Inbound { - return fmt.Sprintf("%d", pkt.info.SrcPort) + return strconv.FormatUint(uint64(pkt.info.SrcPort), 10) } - return fmt.Sprintf("%d", pkt.info.DstPort) + return strconv.FormatUint(uint64(pkt.info.DstPort), 10) } return "-" } @@ -235,10 +236,10 @@ type Packet interface { ExpectInfo() bool // Info. - SetCtx(context.Context) + SetCtx(ctx context.Context) Ctx() context.Context Info() *Info - SetPacketInfo(Info) + SetPacketInfo(info Info) IsInbound() bool IsOutbound() bool SetInbound() @@ -253,8 +254,8 @@ type Packet interface { Payload() []byte // Matching. - MatchesAddress(bool, IPProtocol, *net.IPNet, uint16) bool - MatchesIP(bool, *net.IPNet) bool + MatchesAddress(remote bool, protocol IPProtocol, network *net.IPNet, port uint16) bool + MatchesIP(endpoint bool, network *net.IPNet) bool // Formatting. String() string diff --git a/service/network/socket/socket.go b/service/network/socket/socket.go index 29393de8..0c294d66 100644 --- a/service/network/socket/socket.go +++ b/service/network/socket/socket.go @@ -44,7 +44,7 @@ type Address struct { // Info is a generic interface to both ConnectionInfo and BindInfo. type Info interface { GetPID() int - SetPID(int) + SetPID(pid int) GetUID() int GetUIDandInode() (int, int) } diff --git a/service/process/api.go b/service/process/api.go index b687ae83..a2aca7f6 100644 --- a/service/process/api.go +++ b/service/process/api.go @@ -2,7 +2,6 @@ package process import ( "errors" - "fmt" "net/http" "strconv" @@ -70,7 +69,7 @@ func handleGetProcessesByProfile(ar *api.Request) (any, error) { source := ar.URLVars["source"] id := ar.URLVars["id"] if id == "" || source == "" { - return nil, api.ErrorWithStatus(fmt.Errorf("missing profile source/id"), http.StatusBadRequest) + return nil, api.ErrorWithStatus(errors.New("missing profile source/id"), http.StatusBadRequest) } result := GetProcessesWithProfile(ar.Context(), profile.ProfileSource(source), id, true) diff --git a/service/process/database.go b/service/process/database.go index 82a6dcb8..091d1470 100644 --- a/service/process/database.go +++ b/service/process/database.go @@ -72,7 +72,8 @@ func GetProcessesWithProfile(ctx context.Context, profileSource profile.ProfileS slices.SortFunc[[]*Process, *Process](procs, func(a, b *Process) int { return strings.Compare(a.processKey, b.processKey) }) - slices.CompactFunc[[]*Process, *Process](procs, func(a, b *Process) bool { + + procs = slices.CompactFunc[[]*Process, *Process](procs, func(a, b *Process) bool { return a.processKey == b.processKey }) diff --git a/service/status/module.go b/service/status/module.go index 2465d09b..d10d51dc 100644 --- a/service/status/module.go +++ b/service/status/module.go @@ -40,6 +40,6 @@ func AddToDebugInfo(di *debug.Info) { fmt.Sprintf("Status: %s", netenv.GetOnlineStatus()), debug.UseCodeSection|debug.AddContentLineBreaks, fmt.Sprintf("OnlineStatus: %s", netenv.GetOnlineStatus()), - fmt.Sprintf("CaptivePortal: %s", netenv.GetCaptivePortal().URL), + "CaptivePortal: "+netenv.GetCaptivePortal().URL, ) } diff --git a/service/updates/helper/indexes.go b/service/updates/helper/indexes.go index 7af8a610..8a272ea5 100644 --- a/service/updates/helper/indexes.go +++ b/service/updates/helper/indexes.go @@ -25,6 +25,8 @@ const ( ReleaseChannelSupport = "support" ) +const jsonSuffix = ".json" + // SetIndexes sets the update registry indexes and also configures the registry // to use pre-releases based on the channel. func SetIndexes( @@ -51,12 +53,12 @@ func SetIndexes( // Always add the stable index as a base. registry.AddIndex(updater.Index{ - Path: ReleaseChannelStable + ".json", + Path: ReleaseChannelStable + jsonSuffix, AutoDownload: autoDownload, }) // Add beta index if in beta or staging channel. - indexPath := ReleaseChannelBeta + ".json" + indexPath := ReleaseChannelBeta + jsonSuffix if releaseChannel == ReleaseChannelBeta || releaseChannel == ReleaseChannelStaging || (releaseChannel == "" && indexExists(registry, indexPath)) { @@ -74,7 +76,7 @@ func SetIndexes( } // Add staging index if in staging channel. - indexPath = ReleaseChannelStaging + ".json" + indexPath = ReleaseChannelStaging + jsonSuffix if releaseChannel == ReleaseChannelStaging || (releaseChannel == "" && indexExists(registry, indexPath)) { registry.AddIndex(updater.Index{ @@ -91,7 +93,7 @@ func SetIndexes( } // Add support index if in support channel. - indexPath = ReleaseChannelSupport + ".json" + indexPath = ReleaseChannelSupport + jsonSuffix if releaseChannel == ReleaseChannelSupport || (releaseChannel == "" && indexExists(registry, indexPath)) { registry.AddIndex(updater.Index{ diff --git a/service/updates/main.go b/service/updates/main.go index 95c20f04..218675b8 100644 --- a/service/updates/main.go +++ b/service/updates/main.go @@ -226,7 +226,7 @@ func TriggerUpdate(forceIndexCheck, downloadAll bool) error { updateASAP = true case !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates(): - return fmt.Errorf("automatic updating is disabled") + return errors.New("automatic updating is disabled") default: if forceIndexCheck { @@ -254,7 +254,7 @@ func TriggerUpdate(forceIndexCheck, downloadAll bool) error { func DisableUpdateSchedule() error { switch module.Status() { case modules.StatusStarting, modules.StatusOnline, modules.StatusStopping: - return fmt.Errorf("module already online") + return errors.New("module already online") } disableTaskSchedule = true diff --git a/spn/access/api.go b/spn/access/api.go index e38a8c9f..c97370bc 100644 --- a/spn/access/api.go +++ b/spn/access/api.go @@ -1,6 +1,7 @@ package access import ( + "errors" "fmt" "net/http" @@ -86,7 +87,7 @@ func registerAPIEndpoints() error { DataFunc: func(ar *api.Request) (data []byte, err error) { featureID, ok := ar.URLVars["id"] if !ok { - return nil, fmt.Errorf("invalid feature id") + return nil, errors.New("invalid feature id") } for _, feature := range features { @@ -95,7 +96,7 @@ func registerAPIEndpoints() error { } } - return nil, fmt.Errorf("feature id not found") + return nil, errors.New("feature id not found") }, }); err != nil { return err diff --git a/spn/captain/navigation.go b/spn/captain/navigation.go index e60267fa..f080e0bc 100644 --- a/spn/captain/navigation.go +++ b/spn/captain/navigation.go @@ -128,7 +128,7 @@ findCandidates: if err != nil { return fmt.Errorf("failed to connect to a new home hub - tried %d hubs: %w", tries+1, err) } - return fmt.Errorf("no home hub candidates available") + return errors.New("no home hub candidates available") } func connectToHomeHub(ctx context.Context, dst *hub.Hub) error { @@ -200,7 +200,7 @@ func connectToHomeHub(ctx context.Context, dst *hub.Hub) error { // Set new home on map. ok := navigator.Main.SetHome(dst.ID, homeTerminal) if !ok { - return fmt.Errorf("failed to set home hub on map") + return errors.New("failed to set home hub on map") } // Assign crane to home hub in order to query it later. diff --git a/spn/crew/connect.go b/spn/crew/connect.go index 96239931..4f376e44 100644 --- a/spn/crew/connect.go +++ b/spn/crew/connect.go @@ -82,7 +82,7 @@ func (t *Tunnel) connectWorker(ctx context.Context) (err error) { // TODO: Clean this up. t.connInfo.Lock() defer t.connInfo.Unlock() - t.connInfo.Failed(fmt.Sprintf("SPN failed to establish route: %s", err), "") + t.connInfo.Failed("SPN failed to establish route: "+err.Error(), "") t.connInfo.Save() tracer.Warningf("spn/crew: failed to establish route for %s: %s", t.connInfo, err) @@ -97,7 +97,7 @@ func (t *Tunnel) connectWorker(ctx context.Context) (err error) { t.connInfo.Lock() defer t.connInfo.Unlock() - t.connInfo.Failed(fmt.Sprintf("SPN failed to initialize data tunnel (connect op): %s", tErr.Error()), "") + t.connInfo.Failed("SPN failed to initialize data tunnel (connect op): "+tErr.Error(), "") t.connInfo.Save() // TODO: try with another route? diff --git a/spn/hub/format_test.go b/spn/hub/format_test.go index 62b79635..1e6bf7e2 100644 --- a/spn/hub/format_test.go +++ b/spn/hub/format_test.go @@ -5,7 +5,7 @@ import ( "net" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCheckStringFormat(t *testing.T) { @@ -48,9 +48,9 @@ func TestCheckStringFormat(t *testing.T) { for testCharacter, isPermitted := range testSet { if isPermitted { - assert.NoError(t, checkStringFormat(fmt.Sprintf("test character %q", testCharacter), testCharacter, 3)) + require.NoError(t, checkStringFormat(fmt.Sprintf("test character %q", testCharacter), testCharacter, 3)) } else { - assert.Error(t, checkStringFormat(fmt.Sprintf("test character %q", testCharacter), testCharacter, 3)) + require.Error(t, checkStringFormat(fmt.Sprintf("test character %q", testCharacter), testCharacter, 3)) } } } @@ -59,22 +59,22 @@ func TestCheckIPFormat(t *testing.T) { t.Parallel() // IPv4 - assert.NoError(t, checkIPFormat("test IP 1.1.1.1", net.IPv4(1, 1, 1, 1))) - assert.NoError(t, checkIPFormat("test IP 192.168.1.1", net.IPv4(192, 168, 1, 1))) - assert.Error(t, checkIPFormat("test IP 255.0.0.1", net.IPv4(255, 0, 0, 1))) + require.NoError(t, checkIPFormat("test IP 1.1.1.1", net.IPv4(1, 1, 1, 1))) + require.NoError(t, checkIPFormat("test IP 192.168.1.1", net.IPv4(192, 168, 1, 1))) + require.Error(t, checkIPFormat("test IP 255.0.0.1", net.IPv4(255, 0, 0, 1))) // IPv6 - assert.NoError(t, checkIPFormat("test IP ::1", net.ParseIP("::1"))) - assert.NoError(t, checkIPFormat("test IP 2606:4700:4700::1111", net.ParseIP("2606:4700:4700::1111"))) + require.NoError(t, checkIPFormat("test IP ::1", net.ParseIP("::1"))) + require.NoError(t, checkIPFormat("test IP 2606:4700:4700::1111", net.ParseIP("2606:4700:4700::1111"))) // Invalid - assert.Error(t, checkIPFormat("test IP with length 3", net.IP([]byte{0, 0, 0}))) - assert.Error(t, checkIPFormat("test IP with length 5", net.IP([]byte{0, 0, 0, 0, 0}))) - assert.Error(t, checkIPFormat( + require.Error(t, checkIPFormat("test IP with length 3", net.IP([]byte{0, 0, 0}))) + require.Error(t, checkIPFormat("test IP with length 5", net.IP([]byte{0, 0, 0, 0, 0}))) + require.Error(t, checkIPFormat( "test IP with length 15", net.IP([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}), )) - assert.Error(t, checkIPFormat( + require.Error(t, checkIPFormat( "test IP with length 17", net.IP([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}), )) diff --git a/spn/hub/transport_test.go b/spn/hub/transport_test.go index c885fcfa..9ed31b6f 100644 --- a/spn/hub/transport_test.go +++ b/spn/hub/transport_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func parseT(t *testing.T, definition string) *Transport { @@ -140,8 +141,8 @@ func TestTransportParsing(t *testing.T) { // test invalid - assert.NotEqual(t, parseTError("spn"), nil, "should fail") - assert.NotEqual(t, parseTError("spn:"), nil, "should fail") - assert.NotEqual(t, parseTError("spn:0"), nil, "should fail") - assert.NotEqual(t, parseTError("spn:65536"), nil, "should fail") + require.Error(t, parseTError("spn"), "should fail") + require.Error(t, parseTError("spn:"), "should fail") + require.Error(t, parseTError("spn:0"), "should fail") + require.Error(t, parseTError("spn:65536"), "should fail") } diff --git a/spn/navigator/optimize_region.go b/spn/navigator/optimize_region.go index 14814813..5f362e18 100644 --- a/spn/navigator/optimize_region.go +++ b/spn/navigator/optimize_region.go @@ -210,9 +210,9 @@ func (m *Map) optimizeForSatelliteConnectivity(result *OptimizationResult) { // Add to suggested pins. if len(region.regardedPins) <= region.satelliteMinLanes { - result.addSuggested(fmt.Sprintf("best to region %s", region.ID), region.regardedPins...) + result.addSuggested("best to region "+region.ID, region.regardedPins...) } else { - result.addSuggested(fmt.Sprintf("best to region %s", region.ID), region.regardedPins[:region.satelliteMinLanes]...) + result.addSuggested("best to region "+region.ID, region.regardedPins[:region.satelliteMinLanes]...) } } } diff --git a/spn/navigator/update.go b/spn/navigator/update.go index 73f52811..3fe17074 100644 --- a/spn/navigator/update.go +++ b/spn/navigator/update.go @@ -622,7 +622,7 @@ func (m *Map) updateQuickSettingExcludeCountryList(ctx context.Context, configKe for _, country := range countryList { quickSettings = append(quickSettings, config.QuickSetting{ Name: fmt.Sprintf("Exclude %s (%s)", country.Name, country.Code), - Value: []string{fmt.Sprintf("- %s", country.Code)}, + Value: []string{"- " + country.Code}, Action: config.QuickMergeTop, }) } @@ -700,7 +700,7 @@ func (m *Map) updateSelectRuleCountryList(ctx context.Context, configKey string, selections = append(selections, selectCountry{ QuickSetting: config.QuickSetting{ Name: fmt.Sprintf("%s (%s)", country.Name, country.Code), - Value: []string{fmt.Sprintf("+ %s", country.Code), "- *"}, + Value: []string{"+ " + country.Code, "- *"}, Action: config.QuickReplace, }, FlagID: country.Code, @@ -712,7 +712,7 @@ func (m *Map) updateSelectRuleCountryList(ctx context.Context, configKey string, selections = append(selections, selectCountry{ QuickSetting: config.QuickSetting{ Name: fmt.Sprintf("%s (C:%s)", continent.Name, continent.Code), - Value: []string{fmt.Sprintf("+ C:%s", continent.Code), "- *"}, + Value: []string{"+ C:" + continent.Code, "- *"}, Action: config.QuickReplace, }, }) diff --git a/spn/ships/http_shared_test.go b/spn/ships/http_shared_test.go index e16ff53d..d48417e4 100644 --- a/spn/ships/http_shared_test.go +++ b/spn/ships/http_shared_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestSharedHTTP(t *testing.T) { //nolint:paralleltest // Test checks global state. @@ -11,23 +12,23 @@ func TestSharedHTTP(t *testing.T) { //nolint:paralleltest // Test checks global // Register multiple handlers. err := addHTTPHandler(testPort, "", ServeInfoPage) - assert.NoError(t, err, "should be able to share http listener") + require.NoError(t, err, "should be able to share http listener") err = addHTTPHandler(testPort, "/test", ServeInfoPage) - assert.NoError(t, err, "should be able to share http listener") + require.NoError(t, err, "should be able to share http listener") err = addHTTPHandler(testPort, "/test2", ServeInfoPage) - assert.NoError(t, err, "should be able to share http listener") + require.NoError(t, err, "should be able to share http listener") err = addHTTPHandler(testPort, "/", ServeInfoPage) - assert.Error(t, err, "should fail to register path twice") + require.Error(t, err, "should fail to register path twice") // Unregister - assert.NoError(t, removeHTTPHandler(testPort, "")) - assert.NoError(t, removeHTTPHandler(testPort, "/test")) - assert.NoError(t, removeHTTPHandler(testPort, "/not-registered")) // removing unregistered handler does not error - assert.NoError(t, removeHTTPHandler(testPort, "/test2")) - assert.NoError(t, removeHTTPHandler(testPort, "/not-registered")) // removing unregistered handler does not error + require.NoError(t, removeHTTPHandler(testPort, "")) + require.NoError(t, removeHTTPHandler(testPort, "/test")) + require.NoError(t, removeHTTPHandler(testPort, "/not-registered")) // removing unregistered handler does not error + require.NoError(t, removeHTTPHandler(testPort, "/test2")) + require.NoError(t, removeHTTPHandler(testPort, "/not-registered")) // removing unregistered handler does not error // Check if all handlers are gone again. sharedHTTPServersLock.Lock() defer sharedHTTPServersLock.Unlock() - assert.Equal(t, 0, len(sharedHTTPServers), "shared http handlers should be back to zero") + assert.Empty(t, sharedHTTPServers, "shared http handlers should be back to zero") } diff --git a/spn/sluice/sluice.go b/spn/sluice/sluice.go index 32a33151..bc136b10 100644 --- a/spn/sluice/sluice.go +++ b/spn/sluice/sluice.go @@ -47,7 +47,7 @@ func StartSluice(network, address string) { // Start service worker. module.StartServiceWorker( - fmt.Sprintf("%s sluice listener", s.network), + s.network+" sluice listener", 10*time.Second, s.listenHandler, ) From 0e5b8b2e064fdbb16ce309784ad0dd225e8dc22b Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 28 Mar 2024 14:08:03 +0100 Subject: [PATCH 25/35] Consolidate different licenses to GPLv3 --- LICENSE | 143 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 78 insertions(+), 65 deletions(-) diff --git a/LICENSE b/LICENSE index 0ad25db4..f288702d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies @@ -7,15 +7,17 @@ Preamble - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. + The GNU General Public License is a free, copyleft license for +software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to +the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free -software for all its users. +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you @@ -24,34 +26,44 @@ them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. @@ -60,7 +72,7 @@ modification follow. 0. Definitions. - "This License" refers to version 3 of the GNU Affero General Public License. + "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. @@ -537,45 +549,35 @@ to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. + 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single +under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General +Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published +GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's +versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. @@ -633,29 +635,40 @@ the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. + GNU General Public License for more details. - You should have received a copy of the GNU Affero General Public License + You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see +For more information on this, and how to apply and follow the GNU GPL, see . + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. From 6daea521c341ea1b011cf163b1533f7c7daf1419 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Fri, 29 Mar 2024 09:35:18 +0100 Subject: [PATCH 26/35] Fix netquery textql parser when dealing with IP addresses --- desktop/angular/.eslintrc.json | 51 +++++++++++++++++++ .../src/app/shared/netquery/textql/lexer.ts | 19 ++++--- 2 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 desktop/angular/.eslintrc.json diff --git a/desktop/angular/.eslintrc.json b/desktop/angular/.eslintrc.json new file mode 100644 index 00000000..4a0b4c0e --- /dev/null +++ b/desktop/angular/.eslintrc.json @@ -0,0 +1,51 @@ +{ + "root": true, + "ignorePatterns": [ + "projects/**/*" + ], + "overrides": [ + { + "files": [ + "*.ts" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@angular-eslint/recommended", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "app", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "app", + "style": "kebab-case" + } + ], + "@typescript-eslint/no-explicit-any": "off" + } + }, + { + "files": [ + "*.html" + ], + "extends": [ + "plugin:@angular-eslint/template/recommended", + "plugin:@angular-eslint/template/accessibility" + ], + "rules": { + "@angular-eslint/template/click-events-have-key-events": "off", + "@angular-eslint/template/interactive-supports-focus": "off" + } + } + ] +} diff --git a/desktop/angular/src/app/shared/netquery/textql/lexer.ts b/desktop/angular/src/app/shared/netquery/textql/lexer.ts index 008cbd6e..a3f2fe93 100644 --- a/desktop/angular/src/app/shared/netquery/textql/lexer.ts +++ b/desktop/angular/src/app/shared/netquery/textql/lexer.ts @@ -43,7 +43,7 @@ export class Lexer { } /** reads a number token */ - private readNumber(): Token { + private readNumber(): Token | null { const start = this._input.pos; let has_dot = false; @@ -59,9 +59,10 @@ export class Lexer { return isDigit(ch); }); - if (!this._input.eof() && isIdentChar(this._input.peek())) { - this._input.revert(number.length + 1); - this._input.croak("invalid number character") + if (!this._input.eof() && !isWhitespace(this._input.peek())) { + this._input.revert(number.length); + + return null; } return { @@ -182,13 +183,11 @@ export class Lexer { return this.readString('\'', true); } - try { - if (isDigit(ch)) { - return this.readNumber(); + if (isDigit(ch)) { + const number = this.readNumber(); + if (number !== null) { + return number; } - } catch (err) { - // we ignore that error here as it may only happen for unqoted strings - // that start with a number. } if (ch === ':') { From 3e2b9a9c2972ac13d630474e52e7be4069fc678b Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Fri, 29 Mar 2024 09:36:26 +0100 Subject: [PATCH 27/35] Add remote_ip to full-text search and fix focus in netquery component --- .../src/app/shared/netquery/netquery.component.html | 8 ++++---- .../angular/src/app/shared/netquery/netquery.component.ts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/desktop/angular/src/app/shared/netquery/netquery.component.html b/desktop/angular/src/app/shared/netquery/netquery.component.html index 8a05984b..6b5dfbf0 100644 --- a/desktop/angular/src/app/shared/netquery/netquery.component.html +++ b/desktop/angular/src/app/shared/netquery/netquery.component.html @@ -14,9 +14,9 @@ - +
@@ -129,7 +129,7 @@ Quick Settings
    -
  • {{ qds.name }}
  • @@ -350,7 +350,7 @@ d="M11 3a1 1 0 10-2 0v1a1 1 0 102 0V3zM15.657 5.757a1 1 0 00-1.414-1.414l-.707.707a1 1 0 001.414 1.414l.707-.707zM18 10a1 1 0 01-1 1h-1a1 1 0 110-2h1a1 1 0 011 1zM5.05 6.464A1 1 0 106.464 5.05l-.707-.707a1 1 0 00-1.414 1.414l.707.707zM5 10a1 1 0 01-1 1H3a1 1 0 110-2h1a1 1 0 011 1zM8 16v-1h4v1a2 2 0 11-4 0zM12 14c.015-.34.208-.646.477-.859a4 4 0 10-4.954 0c.27.213.462.519.476.859h4.002z" /> Pro Tip: - +
diff --git a/desktop/angular/src/app/shared/netquery/netquery.component.ts b/desktop/angular/src/app/shared/netquery/netquery.component.ts index d4befb47..a09d18eb 100644 --- a/desktop/angular/src/app/shared/netquery/netquery.component.ts +++ b/desktop/angular/src/app/shared/netquery/netquery.component.ts @@ -39,6 +39,7 @@ const freeTextSearchFields: (keyof Partial)[] = [ 'as_owner', 'path', 'profile_name', + 'remote_ip' ] const groupByKeys: (keyof Partial)[] = [ From 3d88b3fd3b84b6dade5f0bd62d6fb5c1761fda70 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Fri, 29 Mar 2024 09:36:41 +0100 Subject: [PATCH 28/35] Fix focus in sfng-select component --- desktop/angular/projects/safing/ui/src/lib/select/select.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/angular/projects/safing/ui/src/lib/select/select.html b/desktop/angular/projects/safing/ui/src/lib/select/select.html index bccf19af..c3d144eb 100644 --- a/desktop/angular/projects/safing/ui/src/lib/select/select.html +++ b/desktop/angular/projects/safing/ui/src/lib/select/select.html @@ -1,5 +1,5 @@ -
+ Tip + + + + +

+ + + +
{{ urlText }} + +
+
+ +
+ +
+

_eNyXzagA{f)|3`u?_fSz$N2_=$dbrUX zrJ<{fQABI%YA9-}xM_K+sOWgA>-;PCA9DVLq5MDNrNN_(l7-K2{84}DJ?9bhHv)-2 z=8qm4^E)pX!ZE*N+QS|GSF_K*8QuTLkALw;cze)<{tuD^6mP?P|5JR|UaDbl#IxrCHgF*j} zMdfgR_rF%5zav~CG(h5?8R)-to4ZG_&%eaMziRm#g2exi`u^9b`_B^MpZ@=MQTKms znEuEkf2ITF|97VN^BwW0FU^0C&L3-~-oMQW|6v@`7Ee7|e*a%GzNd$~mYTYTo1!MI zKBA>-4Gl#dZ8u%ole#7bt*WV`=Bf2Ns((uh`2U*me@D?DZupn_=pRkAqU-nL-_;)N zEFN3x+062+SB97Zf7A}rNG>V=d>{?x}ndJp1O5IEnlVEC*Y|%ar zSeOVJt<#Xn(`ITV!s zz<9ov4S9AqVUe^pkf038y?t1{ukJ@07|if^V2V&X!GyCg5F)Z{Mxy*stc0TkhuIQe z;5F{vgA!p@RyonDL`11>;0;mowRp=}Wb<&VGB5>t0(qLKeTo_@8p(9WHJTL`^bl$y zW%!zy7P*5IM{?W&ZX7$<%O@8_?93+=C@36cautqaEBk-;vkk;9oljzIlD zV=cQCxWjb`xb!Ii@!nzq-Up*Bl{zX zhPd9ukB@Yem-UFZ>H9(2AZ>6a<{0dm!C7WACLE6$L@S{+X0}8hoZ~E+jeP&+AkApt{(I^Y0b zx)&AK-C%>2dEJ9EMm>kXm2Ls}ydt7UOc%;8$}H7WHBc^4S4;lGl5hr3Iu~I1DG=Lo zXi0vkKle_?Er!zom(J(}ZJrZK=SimZzII>mr$SMwAqsHS7`9_V&8&?*x_sTbG$;f1 zlrmBqX}6DvckgzpZu_82bm)*5y44{Ey2-iOxwJXPrlH#f&koZDPoo#F^Y0|XqiFG$ ztp*f~QK1Bx%Vf#yXEBLmV1744dc+*9Yfh7u)+OukRT+*pWGPJJWi?tBNpru$1w0-! zp~R8oNJBJbJRhHuy-CW1@#9&QvY?xn*%&gQMk$(0FqrETz!-AK_3A6X<%R^nKVtD zJ3LNSwAeE-0QAmEkY~=PiKewGBCt z+C_{qhGh*tM_J)Gpre4;D%;5uGTtZWQKan#!Q{d5Nr?uYP-msTeQygT6X)+ke?aO82G7WY0&k zKWgtj-z#`_p(QDXQ1GTHk$o&sib)9c_Bs=pt9atWHCIIYuJVxDe)zIUxe=}XRav4j z5L23DgJ{1bF3jfLzDw_|AyIY&LPGj`0jz*-k$~ER5Zmsff&;dh0zRxR(HK^Bju8uNd{lM%iT@^br_-g z^yxiYVOLm6KMzF(b#x^uK-P{)8u%IrUx`7fFPk6`yDH4SC$xAPdA+yTPVU~{nP{v@ z=iMoyh~n`UxBc6PZyHaE;@3}00;|JJ+v@eI*YOrZr#l?8T9a#dZoZWT;Xj>B97Bmq zz_O}^z|GtRLfguOc5RVa8zvK{WQZ4N44~KRg2S_Ki6$-*A6yC0>H3`@$BA@WlL@yX zB;at+X?k{s;^S4&`=w9`_LCs~GWi5pcLR)k%S0wHEkxm31(bAwd&P{xPg0?UBVynK z>jE2=^*gJ=DqBV4Gu99N78DuMUeN?6Y&BSB1x-OOo1|Ju0!CVNN^Qelsd%2j$m*EV zIk1Q<$YSgj=_hGwG!c(SpmZEW8acmNnb_TjN;r#ylt}e!yfE0OE1HJFbl>rgj1?y| zf!Q6s6_3)7`kdX*`vx;=2aNN;JvjD-kg;S<4ie9!6pvPRUB5&`pg8Ma^xy|n>(=`}k{n3_Sb+%SZ2!sI@t(HM3BGc5`nZX+&L*~} zfOetl&(}x3=yRsH=o1(t*(2-M`=^mni6vL1K_OPg{AG#cYYCyVNJzm_KI7?jp|;cV z2T^HON!ils#PfD@{G50$T;M~1AABi>0etS-Bi2;$uoAP%lwXP3&n1tbxL!&hC$^f`kDuQ@L)cZHkAh%{A2 zvETX0?{Lc;QwEub6rgH|4fI+aLR}U?ieRIZ^oOUe+ybPQ8LI~pZiR?szse)B>|@9x zQ=(+&LwGgknM!qNl`5H?47!sc&6s1W>IN$U#WKX@a+N_PE_R{);;u(Cz#q##x%7%R zk~0#@t9VTdBrHk>f1_=TP}RPO?1#&c60bq9!{aP2++-}+cd=?(qjj`HFL#8+>H2fk zqBOSzc&!P4JZOQv z-G(MFg%r3QFiVNozR6|k+hCWfx&?f$g>4^kD3brG4Qb?SWcqeLlo+7HMd~9hk>8T^ zKj59kzC%UQ(?6$_-{;-tJ=XMi_U-7$l=Jf?SDgjSCCpSg=Rd7_Q2epClehNHo?Pte zMT#?JQRsozH&p8;#2|loi$o7@?V|S9mc7J? zIlPSk6Z+CBzY#^;}M4!b@{;BK$n~IBw|jL_0sVbFD#XQd8;AgFtaH!EGAmj zta31QYX50qb4u^x^3bmyS-Xbx#3!ILm(d@(VDV;LLiD~GHzGdqoR-=ZDMFI$r`R}#4AY42=0Oy}3qp1j5#TcH~KCSVfy zG+!k%kfy$x@M{jaflQ1m%{(DGA|;lNR8BF5u&huEXOJ6>^a9^$WOey#{^#_@b!*#} z>Je`BqV)j>Zu`dSdJbZF_oYna1J&}sr2X`U)OI;G)UR*cPeFLf2O0^%@UuGN&l7Tu znmL^JF}jCLA_H4J(p)VyMCp!Qmy`(|VQJ97RnrdyXVxxfQWN2g6+DW_y>bDidssq* zix-i_INMU!=Ch^OZX?zzKW#!OCw!49dT=rhp#=tT?$hq%?*Z1U8CO-vISV#){#+haY zQ$Q1~$x?}}GdDM?l_@RqJJ4K4&9f-y)B-61!>d#*-{Ku6?8oI}QZEJ=_xY&Wt{*dG z5;Fm|gI6cuogu)`tD(IR$7d5ppA`wVuTrT6wH7D!S3;g$Xs}2~2A4oQC#WwJ2ZHES zO0Rr5skQ{g%Hg+!Yh}BNNJhLZYH+)Hra;xhk}LLi6?N%k7+fFD>3yytQmuFVziJ|E3kD<(T4(YI$zV^W32LiN`C>8oiCl_6iZkfT# z;NMB@y}gA|(n@%&6c6v#2SasZmBspnUnb<5EV~OQW9N?203a z-F_ti&wgY{Hrg5;gG3pyjCA_7^KGz#92;7rx)hy%wp!kWsxo zcvF^@&cB`7o6|MHOW96!WrA70t|j9Ox3)~hGB2Dxi+Z>(K-IPu%M?C4hRs5zAE)cT zmr+0*bmw|8v6}F7a&ZP4pTvBCsH3=}#16-Vqx9n)I(;SB>)SW;w&KesXCt|NOBCQ= ziRvm4>T+hY=&sU@&x{nY{V|CsJuztglPNRKEbkk1O0TIgQfIobsDjHbP|L3V)r64A z*=-h;+R6(i2)9qnvE0<<90Sn^@axJbm{j%FeNSUc(PISV8@ZJ(AXv&*2AuA8LhIzu z?VzwTtx0vND|&|kcmoj@9EZc!dYXm#0$`9#-8wCnp+#{XG(56J0N(w44WRO_49Cv2)i?&BxN7%3dv3Coi|8il-TmuB;%7MK*? zP}y#7OfaK*ip9S&ugY0`?>4l;OH-a@dn+mY44< zh=(NlRmF`z2sdt{n)R=JWQt{Bb56ngDK>LbmmesIH-I3&yQ8n8tp=N2SE|MQNIru` zv#^U0?{)SQQeK=|NheaofEJSxv}#8vS0nY2INAqQoFZR}7A4)Mzx9%06@Hz6)qWWUJH}a#e?C zhD4?78&_q}Dc!C3J|acgBOQ=T$=Bx(2Z(tXCwP>WU?T^0xu#m|?p@}$@0>s9lWLG( zj34(^A+!3^`-1Gt1S7RqU|7=}59HbfEkKw}2Xu*99^Rb~bz;R%i(1etzVUDLUu7>h zwjKr9&%}@tM~}OI@Fm6I3YsC0+m9lzR!k5ohu63sEa?%VZ$^@%$nj)oub$ z#F=&%I-rfG*%E~b zhO7vwi!6!0gmKbFA*MTjl^j9t9Zg)9-h`s0R)kSkLPRe95^`9HxE>+Fn}1_uqm?7( zjMDUC#6i_OV2(*K%{A9S)VUwg;vxtOF+CcpC!HsIhMi44sNK!+X0LR7;`VF0A%?;3 zo+R!U;r^?^>D|4jtU4lLKe(7?!?5zJv~pX`Q%5UKbTLe83WifmbS+ytHF(D~ocVAF zc*Ppa^KCFFiY@cwwHD(yZV_?^=|Xr5zbVKUx0GGvG_uZJ5?e9I&;psYtY#ts*j{Sx zXrlYu*oATGnPn4sg5ruZilbT`t@#^n$CQj~+mLdag#ZhoCTYu57=NBtMbA2hK0ifd zw{B)Qe{caK7%mgnLbEi|IFQ|8v6Zn2_oZxquM1Pg4Cm@oM+U8)p4O>z=US0omWbw> zU?k;}N)pe~R3d~|f+#1JO?Fl3t5%FbF=NlAx5=WUVRCjsHIGrM`=Bs+?mq1}rH|;| zse!$`NjI(I0Hg6wCvLP{eS1?K_p9aVOqV^S0;%2QLH4w<^o3-!oAV*0s+$rq1ip!Z zRzB>5PpD;+muY@b)Fn#Sqm?IiV|1^6h$4*HL46%lvw8FeXJqQJp*lmtrK6AOV6)#& zQ+yWGN5_Rv9X;J%{B*ByBGL-WM0!GKGMgP+Ciul+T0UrkD!?W5^jQlx69R@4*$8f= zyK;qR%4fu0UyibWdj9S#sRUnsE$TEANfqHJ3%gf^+~OJ=uMm_JoKw|lLH%w2@7StY5`&y?<|U1DA&JEJ*aO} zIuuKfdP$I?=zMNF0jt(fRkc=LE~3>2kTzYu^65h3cce6xp}i+4ImYM>(k zw%U)?pXpfJ%)y7(n89WPYicV7^>TOD{lO~=8P89RGLbR~J8rWh=gHq44G-2#z3Gft zVxeh@_1h$+-5jEFnM@%DCYOEk6!rUSbv!Fs%;)ZIpNYDLg z-7exwzG}#iJz`%xaT5*{Obr87FP*1E(k!yRvtwDYKSV7$%rPT80l#!!TmXMy-4}@o zvFPUmEq7$&?yz=D)sC_$963~s)F;M)hY1_+;Mp1M)r=7d3+l3l-++n{a7!w%MYkEG z7vT`g-q@p$A7pKFCi)zluW+$?(~=~f_XZ=db+26G!b!G#epO5avJ3sHx5Kj zHs-c73UGK&pCMh#^4SEdG0qA9O1f|EOx_C+(PHMDH)4iIg{c$xRYvu1T3EL`#I4=bz{0#Bz7DPNdFIE3}If3}aFC|3563L9E6Zs)>-2$cv*c=&+JKbE9K4?{b zv=>^+>np%*y45CdAXc!KX-3PM!~O27GoSE zCVEK~=kzWNJ%f};U8yMKYYN6B`}ZyS_B&rj1Ph2;Kdi|yYs=)Z3y4@lNOekdN3}m2 zMHf6On|xSj%wI5(dpj3BLh#>Whgt!xEFu>1%E>>Q4lm$Xa8_r3WLaR%p9b%tF3y5b z{9wf{c3nCZ!7ACj^9MK`T6CXZVaOUoQ`elStpvn6$IQ4gSD7x^9%ocim7fuJk^!a$ z+UBhPe99poRJbUL=uRZ_-SV3Zsi8_#U)hWyc@fd(u#_*fBhu01h#Aa1F`dsmBq7oW zd5YdmFVU2!{i30l2ye{lID7qOxj7b6)|B{^P;P~&LxbxLqMd1;tAh&DHOkF0uxo+? zrBqZuV7vy!QzpiZ>9?74%YQD8kEIT3?z11O``qa&b;+f}L8|-I)x?Pt27!1OF8IN7 zdA$qKbn{8u%^@P`qz2@Rwll@7?ZwkiQzSofWeu}JYoVL8O)8v)7>)DFiT%Jy8`67e z)VWug)J}1V09FWohM#I)Jv8Zptvc(xZ&v3gxDsa1Ngh$b@_#~HvjDE}D!6|(B=xsk z1?oGa_opBCxN_H5jXfZbEn1ERBBYKqfTyDvV0SSDgoLW43{U{XNbTRbIF!=c5aU%> zmZ%I!;&}UrQXasN;lgz!rsNv!%-5pbO;9(zlb+1z@0M)?WV}i3>2QKNO^cXv4`oD! ztUVn~09KB!m{q?x;}W6BBUd*~d?n|WPdJO~(Az$G5vs%L8~8NpO6FY;7NW!3QT<*~ za4dI|#ng`zy3k1GZ;ck`58*pnjE!u^aNNSyqwJBgDGXM@P*gzZ>hjX`w(CCaxY%F& zW@cJKR2;SIW2b95W3C%r;;_P7R+3PoG`t&1>41tKn=fXFpA#`DTNA>Y|5t|d|7A>WL0!Y)*b!60vBt?2!VDxJGJNLMp5_5(57Vx$> zcF7fyTE~c1zK9Go1pBLi*S)DfK~jrhTLvqj@-_nr z=W5Z%^|V~YzlD)Ky}~TvAOYs)V~JheY0E#Fq-yAN*pO-1{sK+6ggh2pKu&9YC)}=R z*-bb$#cxXM#V7-ffhpLbu&qD@LJ8q4UvZ~9P=Gyv+$}zVWM?hU*~1Hy4@|#tjPm`LC|f{t8o{G zsz;$HYgDVAxTNwD_;6h0b+>!dT&uw}Ri7e4JJypjI^(8a#f(i&-~3HVF+n*MWoGdk zleXHMWC+GzYi{|GDi4%9k`I18^Gd`^O^cvvBRFB+bnLa$pgBt*pYVY3NIg$q{-wk! z(SBoS(_Vmd0e>Gowtv1A&7Z)wmu3-AZ2lbZU^sj73@(}r$K>7WN!Zy8Sy6l$V@#m? z+Oj9k_#IcXu`#M+i5TBvv1b7YFhX2StJ4`Ufq!_Lff7%7k}+MvDqSjvL!GC34iiqYi-JnqFE8b!7zHg zCH|ssj=n%6*LFJXbT*sCc#B;EJn&z1Cip&9=8QmJKys`MNvrD~Z=;T8vKQ zc)ikLFFBUF^|84XpQH)d&Wzyft8agZKW1kQCdk$QIGyFp{61JOVrR+tn4mx8*q1%y zJ8Lf(9>yBMRvWqM6P>v1?a_lkK}_4>(Wg@K~FJM%>38rJLCx$=%c4n7eMpU`;t2y#lp!i7|8@8Eo|U@^0dJy}-F=o?c^iR9z0MGS z##m0_L)ROM7Yzl!HJ(DGPDG4?bcC<*LN=>tj2tLL&BsZS8-=SPWI4sTqfTwhSYW=l zXzG_H-#5M}o?{JW{;%sT4+ScnY31i$;2;oLCH&14livMqUzM zWwAc(l?Mst(fXlmpp35i4yU+VA5*RP&F+mbXkJn))$WJBTm0S8bFA5qNacw=6%Yjx8BB@ByW+^FM}yK+5|q4{c{ zh5-3xt9T{tAA0Ebf|V;T5u_yie9^zC7a#2hbY$NOA$nT+klC#O`I>a^u2gPgyx|jg zwyX2@pkJq3$zgmcP30C8++Am^U$kX{vaSo+N|2dVY`w^!Ltg-%c{IK?Ox)Vv!^J^5 zq~WWL$!-x6BcQ6Dx$_B3ChDDHYDIMWUw$-SO>80XD{sCrWkHC~dXGvO+6diK?= zz6jZ&nTq1aS?mfh9p-PY#}R84+}qb_ob(X=%?-$bG|-eQR*u&rE_|F|-$zwfge*i* ze`X}KN)|m%oh8MSixOJ;t!1>N1EDKoyMprES*KsZB712*j;oEGQwGg6q^fke0RCY* z%ih>w>GU7#(QKrtgHJQt+RQpaO|~+(v7v4)V0LguCzlYcJF~(fLA~w{)<)cVfA1&A~;WTEO&l zdYSn0<>>L&d(0lP^=D3H(v%-P_x+IClbE1e7y+R{-+WG0B+pm~eUr=tXy3ZE81M3g zp{oSL-@K$}XNy#VS@wsJ@&0Ew92q`b0NYM(wl<8b@3%tg6Rj77sn1Y!***(RaVqtN z(&_p5U@n4UebLGzo<2D^`uW*~L7=4;I&%y(6S8B{7sV>5iTd?{ICh{74#B0_>3lhg zl$6h`x%n-H`kXWC-S-n(%vGl3LKiLiIYp2_QjMANeGk$I`}7DW8Na=J$bez8n;C%> z5Tv6a77fDVCX^tG0m?FUnl~$O>GeZ!syDPgY1c2>3aIbME#v+8l*D@d*HVg~pK!>k ztGWvRb`10MFt0GkLfto@^Py&68Celwwl>9vy;=&t+_||PRWQnhd(0C7DZOr)AlwZi zH;1UN$^ctj@p&~rNQX(v`}4JeNXC77UwQwWe$mgeCB$dc5eJHiaN)l_kah`j~+n?7^ z8sL`oKW3k)-BnK(>b;Xj6(WweekhZSL0LHrAeq17(qHZi&ihKhceZ3ZoqwqNZ5YqU zfn+a(DDc?al!YW|Csi4sD9LrVK`VT#WXS$4P(AX?RIca@V&Bj_h$U{U{)H>e98WnY zSJ^b7M)H#e5BW}`@}VMf_4JIQ<`yZJ`aqS3_EESdt=Z}hZEFcFUT1IbepnsedjU)s zP)FtzXw+o#I=wNV8UqD;t~@IuzNi9;2SKX=&|ENonN0%RZ1Ur*CcQ6sqKVoz$3#*| zZ30t_u7~*JTV#p#RKGX*@DZVHgHMUX@*KwJVC|ek?@WBhy+=K93~nRm$LgZNgd?3J z^c(Cn7VGc6)Jue&x|`2oiX{K?9O&%}>fYTY7D0y4_gn1k$JMpiR&ocqan?lT?)w){ zZ|g1%J_lcdZLK88XgTrKsRRgVDe&sSn|2?Q9=C}~!cN=ma{- zbJW-4um{et(Nsmyjq{QY{Hl@^-F)1l<@8bl)5@7~>w1KgGYhWYZR6xgRC=FVafbCf zM{=DnY7mNI9ZIk9c9A?_!wq%rj&{A=e?k|TMQ~ozYAe3_LHs2jB|i1rffXXU0sM_= zT?HGz&q|5oy0%(^ zo94`ly+c=+GAJ#|>||=B%@k9`Nr(Xx_=5N!TvJP1&%0KW*-Px&otl%t-d^C0Cf}8( z9fN3?zdd0N>&|1R$Wu^QpZzmT-ZLX!6LtF=I`F9_`F;(BMB6hx2k{UtR6d2&0A4J zHVe5}+7-~AOT9L_*+AELEwaQ5)?KVc0as`CG`*c-dlR`3vt0u%O?1lup5+{Jky4P- zp4CJzvScl@GejjXV0Xmxbr8 z!9W01^{(9}@4cf7x_1RvL<(T9HUe03JDOoDB5=4f$(4{kk^vRlV5@*D-ZJ0sVIoPS zeuYpjwuir*@7^VgUcMm-^pY#_cR~F`%UOea8el8pbE>Kl?beU+PXx+}5;?vk{o#~P zM{6O#6UIb&T5f!De`wz-DWCAh;@(o>Nw))z6GqeNNa~gQo4b54<08^BA$`L}rmyIv zu-wQ>9yd<4Bjzl#+@ds2k-TqJ9r3V;9Kt34DvY@0I0L1eZRZzE#jihP(_y)^8YZ@? zXy(GA0lvAJ-=|4zq6i%O)H6>t^Rhs;C?+maE7}@NW<(eiQNgp{Z z^X9vGm?QQQ;$oOj_(8Shp9y1Z8YqXw7@At6@p!2k?rq&6$L&g%NkpJNT}Da=2iS z^CKt<>fpsSw>+A}8`j6Nz72=_>`4!q%)A0J4Fv?q7INH-fmg^6TEHlkO{XqBuu4f_ z5BxgUCXJ!D;M%)fGlYQw{Q~Pg&OM+sUoIVZ28M9OQmQTVK?2szBYeV%oew0FevnMs zGk|80!XFA;av>#q^tOaBRxQRH! zV3zHzxLK)&3KQQbUXzbn;5qog&iK6~XZj17O6{8nM%u0rGSuwtQrg82OXj?AxyC)W)C>}aTg4zwB05a<1NDZ~ON-~pl=~S# zC|B)tTtG64zlxmvSeVfC%a;Gc(P}`=9sD8L96*#KmFUCEHx<7#5>-@t%qjMo9y3J9}pm6)aA=d|&<}^4UFV zol?7B%(nu*ace-2sQ_S=z%E`LqNd?V;ab+2eEI&mS=6# zZZN`B8&D&VlEvw1np2>qIq$ApW)Bpu?Nknm%>-RyYTZ3zzW|3{T<6J!ejn-T*Pw;T z3TFt>ga3!peV^to+C4{n^_(V3nKJBhm~cVTq3~$7htHX5Jl?$xPGO|1(Y_*^q|=D- zlNW?n#3=29)RU6(rP&LV8(!DwG&&q6%Oc2PKp zsU8ce%NIOXG2-+YS_*5Dc;KVEYV?IIY5t4rXq4*bUnAGz?y@21YV24ciM@%BNqnSa zk~zse`|dCnYJU5$WxtV>mA@ zhZU87A}aE+$%2s1Z=n*C3@t1LeS+-o*&@ZVG^$w^?DWFv9%dcDzZ^w<8VCxV?H^8L zAimmY$j!K@^pO|&f-#V8_CjZj{PNk7hn>|5$!T)1nfBt$9|s4hbUlK<{XMStO4!!K zEYh#Uz9U?xj)mVdE)B)b3fo-HPxL$0RdFg|laqG2w(*)8gG86_Yp zNU1G5QCoE4-EDL1cB?lNP@d3s=S;IC;G9&%b@wD$C`YM19fkjVnKMu%oPUpb#TVro zy86U{o4^sMv}xeN(p<7W-xyp0yY!OyU7wSImV`p^w8V9y(I<-zc`g2t4~`v+JbwU4 z(Vt5UA>6NLA-*`c(*Wtu=3Cg=H_T-%yvlp(6q^uHbJ(gG5_B^hOh_W|w?&7d%y9Rn zKJuf^N!&9B>EWJRw$_@KV;NUq6r1Xo2i)bxy+{p7!t@RZd-2D~4-qEkY4_aE!MJgn zc1B~&*t?E5m`r4Hfg7yr+rNmkp=Bg_x7WCH&h{cHmq;vR8{cnx$>)&Y(EU^v^1SMh zb|F?a7_6f9^Vf4s$Pd*%Dy ziCpbRM6DAPNn3JsW?gQ4BkJLhCjokHe|vYj-2J5erUcQ)YX!SuUu0u%zz;Tw zA&gBTwPG|^eRkZ{eIV(6lEkL~!fSSCZ?ge?Krz%tVo!M0qF*TS9Ko5{I4z7@v2ZOf z2)3u7okc4-N(g>I^Jo3(HH0A=(sn-lCGY4?yIMz1DMa236 zT6R7}#q|9@J)L(vmGA$@?=y^J#UZje_KuKEWhSE(r;vlnIH@CF@>&tIK$nQP)-)?IA()z{{XeM?GcXCej@jk;Mf(KBS+$LwHKi!}6Pn%as^`@IfzeX8VxJkCGmtMhsqi(gy!%=0 z1)Fp8BFR_!E2-Pl*l8W~1zVt=Ts-sCM|StsVl#@#HmILUI9 zXn`6EB5y~9pXdk7Y83+IY85gp&lw@dPH_FpsFwrGL|}x~n3HckS7;PjKorYq^06XJ zyrU>NTVOgIZ@8EXy>u!1cx}*abE5Pu74pVVLlfeqMKAQll#ze=K-YOanR@d2<4uG0 zc=k<^Piji=-}JERxPl^#5^4c?WBMf|GrGElq$%Jx&-b2@+^wbfgtnJk2xuF$1b`uLJTou# zQH9-^YQrOv!Rb5nW?%;?6M}xrJQugcv}h9jdaGkv9rw;c;%id&o+cQKXEm7e_Y-C{ z<_t8R;X{Pq{~9E5NxbN)m&YuW%ltV?-PYDCPna>{R9<40hKofcZJ-y=nUM8YyTB>P z@#7ec(9b-~<4?W!VNcF&x;IYomT}Ffhft);Qq;C>qOo^dqmEZyZWBU0zXRMWL^P$>mAPGct_JgpWRc7v93yXp@ZAGQnyB(cb~@BhKL zPBnBz2@`+i6{qmNEqVL4ft2i!iBT<1eiySWQfs(ik{sgn3>6u{HoYlVt9i8IC4o!% z2SZ{efK#Ous)@*IE5>BiB2_Ve3PC8gD#-U*<|wv>0a*^y8YDqm{h%JRx9%N;;>CN@ z@cs7_k9`cq=c%*t@R^o956*KMl*Ct-5-i!vIYZ58g02rU`c4;PEOCsm#+0H9`wOaO zCG#466}Iv!0~c~VcO{HWsh3zTe(wdRn{INag8R6~$>&#d0p+$F0!D}x59MLX$b+DI z6*DlZ~&lFh~j7O6D9kY;D!IfgD{y8lOD|{Z(y}hGO%cMXPzRA}h4%Q-Le{R#&@E`TpeF))@ zE^rS@QNUcx>ziQp&Rlb>58)ZXItfLZ@D>(xs8S*a_KY~m<1JL(!lDu^3Pyupxuzc> z#>{s*2)O%OEC}J!z{+8)`x$|mo~}GUcqycxgJehvMX~(U&-$!)IEIC{$D}J;)cQur zH8W-dDHY80BWW$U_%jF+FbFp?(G&QiPu!>FiKw(~nZ~01j>vo;Mly^SB}_H`3R9=$ z&hEzf7~kM*;Qvl!;7@Bj3%%n0H43$EU}HTK>xApbxoj5ond1J=fv&*U{fe13^dkRU z2|t3;WB2rUB9s!Er^=h0%|wKU=q!4W_@CTmSitlsYp=wm9&K*QP(_wwcM4={JfCrQ zx^v!qlhu?U-ZA}JI!BU0z<0FD^AaN%O6BC zjSk4a*#t2-yJ2FQJTHVX(T?W=X`P#0cgITgXcG`)kr~AecPV*8rsUOtDIsz$25;>2T z#qlh(tIBWxaLO3Iq$dV%M!67V64f7MUicA^nVH*u)Z;txyA|nnO|DpG=J|mgVy2Ba z3aU)ARY<~|)2yI|&3w8f{-hv&W%j2NhrrEVtlcdy(XYFDwUXUO`fN;u$Uo+WsZ%PG z@FJ_Fo$t|t!kqc14XgmF-yj=4Lk5npYPpO_xr`=slS^eNXPi@if7hg)oBQz8_j|0# zzUyaH@D+&}e8TA6n7u`**WpytSC()H+oYQEVK@z1$+b zP~P71;J&4pCd{pTEw6m@Ml479X|?KzyI%r4#J#Rd!m5OX!MxVDj+GYCyw=0zcL^o>JzAOG0f8{^u6 z=cl`c6eKSNO!0m4`McoO?tq{_@Qp9dMbh}^tlxZ00_60H_I*hTc3J|}{P@a!80|bQ zfjEu6t$h79FpUf26LVD&57qG!8f^9TG;JT+ru2B4L4pw|&0Kk)L^*;}DGTZ8G>6sy z-Pclf+Gv5r9&+wau>SVCPbv>^N{EBd~Z0bEjcdz*T&|uGjWZe zRv!eii1Z|AIob~n2`oy?y$=x#G#LD((>)*Usl_ttupK>jEc*i+(d(nR)N^0X=V@vp zJg9|V)ah!snu%Q(>)0lmVeFBX%Iivm5Q>eLV`bo{PrfgdZi9p%Yi5KT^L?AF9}?!~_-LeeG&>JVqY zp6PtMJsd(6+2Cx|OmBnmeZq%?JRe6i&PIFvci};IacBmu` z5r`LsG*@ro+cYQKP)4`AT997CL`Iu7H9sqi8(+1uv}anoaUrr0iobEfF1!EvcsHf} zs${J+dB{j@EdvOJ4}D+Sj;dx4B>&nR?d>qOQbQPbqQ?>kjD@m07ZNOr(_1XRKxRY# zR z^^Nu6*H1E0IpVHVS?nZCwUdpR#eR5tdWH}brCShL>%@NtDoS3QnNN_t1B|6d!|(aC zG>+=CqOPF=WwccSr5s~=-SmGLEWG_T-3UQNpAYr;p;4d*Ua%lv&E8$uyo=;SzEdTR z>(1A^`0leu#60$9@t8d4{^CDqRnFrDq8~6@`bynvc{T`MEj-t{W!;tNMR0=~!xui3 zvN2Z&NAAdw@{ihONi=cZGcAeFTs`+VcYtM+XCSz?!4AgTcr$yzAjw}flz&|pnNkiROpoa+PnKvQzwCwVWy)KtS`db#D+8K8(s zm5sr;7^X*l7Qv0VK>Y+UG_1a?LeIno^~E#cr0d+ZnR9_qHMOGV_7J_6sHb%-L?W*Ssw$T?odhbXdI0C0Wt{Vy-z9K*%=n;dpXy{#IRtS z?kbQ3?Fhw?WPH^<4Vr~+jCz<==qD0(39 zw(VTMm={Un%vj^XpbPc|x*m6BUxt(6I!ez&uG+8%a*=Z*B;eSHFa3mc;@xkv8odXd z9W%9j+c2c%&G+QE17R_WMH0ZH=1?S9tWX`mIHIj+Z_0bH?CmzU0%KsSQ{E)AK!;D$ z-D!6TD~&hh>s}LgiO;Fi&zSrkZz=|c%|wo=)bl|fgFoNkpfl9L(2?Dy{Um?1wBfe) zzxpYk7~FBZg*Evq$sa`H5t(<%HwKRp{fR^_^-`aw&{)x5+9B!wAFP1E%y0pyMsS}X z^4TlV*Oy%<)`}>@nlZ-v9SM(zivM_n61#0w+jR7^69Yo9PXr zc6zrjKK{DdjF%o^o>1j3>=wu5_n@$BrsC%CLa3$O%m#wO_gEue(E~w-<}P!{uxw207+$Kc`u&K;`)42 z={8$rTcu-W&nX`y z?$&>kYQKmC@MfSNq&lxeF%-yE5fZu@Xr0>CMXT}MQ~rs!D4$}UN!`H+i=2Yf*)DBd zPCWr`g8UQ2-|doMDX0Mn&$Y)6W+^LS{Whj21|Kxb30(+bAtO=1iCJPd8wY;cdj^EMz^Rm1+g(Uj{36Lb{ zdW#YoS0w04xCCe(!Zim+jaJ7JiqHgUVd+#$m#NMIK$)dEg$-V=j%G-a^Fl<_M>za; zUP~v2bZk9%)d7djYw3siEu?FJR}9M?bw3~h2!~Ym8sf;LJ6}cpi_(CVfqoNH#~SG= zBtR3vUnQKsE=oIxlY?GI;Uu$MT6bd41XF(xJ*Dyz-hp{%wSsFtzy%PaF)TkxJbs({ z4_7BnMG3+3e;sVWm1}b-05m3qKviQ;P#`2jZ$bpUF(k{)eA&(W^OBVwcF>>v;ft04 zn9F4JN7R%#v%~TH98}kmEQ?<-o;T!Oesgo8@L-{iJ~#pcA`v>GYs@b?^uRxS0jabF zkc*m@qseKr87t|-W+L69aTlT(p&l-Znwn=a?`71qs@&kG?NS3{M+)~itf9YZ`te`C z%yZEIx%-CZVf(_fRCFtEBf_e6f?C@p_L;hT4`}@N-ycN}q6vAFmw^ADONGImVE@Bk ztUgD>Hx}0>#O6aMxUWSYd_4w*HPALxq~$>y1A8oxzxIA0zuW%a91A{mvVQ-p?KG!* z>3{qF3+Xb@3^({KYMP|wv3vL{6SHn-P6-cn+?a7Vqw+`SM7zzSC7R0sho$uGDMZNY z0^kdiQEIqS`A{m>6)cz_N?)2^c44v>!xwQwZJ4_6yhRiBbkbzo{*ksB*!qKsX zC{f3d9oeQWKYJdm&rh+Nb8-N18}S%A^yQLZ)9y;hf`^_~@0B3TOEOfse=&f)_WaQ+ zn4{V>&$4-*gOv}uebNXG5R5${qR&AkgNfjCupX+o_;Z+cl7=Nc7?_6~D+%RZD?LF7 z9|N&)j`XQzv-u$D;y9&6li(}N;Ty4UBP3A5tawflwz)K|#hLf(oR%I;t&a(>c_San zL&@KHL0ol&2qq_GT0X95MFS4hRxX*oxp^_C6h7o`78X^_QOC)^1d7m);cf9iuE>DC z;?d#>Kx&0>aNXr^f8Nh2LjrSZ>$-~)!PhivQT>M~?^YV4#2C7Cb<*4RHSi?<4~X;K z&d9JMS@m)8j8U|L}D|3po{YT~w8TsY4xamwT$r^q~i%#_uNE*ho8 z$t8JcA@0XtmOa1p`eI<&e{UT91iev+u#kkC9f?@sl1{!1gJQ{0RT$qJxHF&+k8!t_ z`CM@~uRo&UhZG?X{vr1u3!Mo}WmlD$m!ynPu0KzgXh8wgNB@jJP6pzY;}hq*j*jYQ z-+O~AeF)0k87EFU)VA`KQc}z-yj#aD=W603@b2#ykqf_TI&Ghi z3M8jo>AQ+`Kb{Sp^mEpcD6-n7*gU^_&h@=lpRe7`7NL%X>Th>;M(+U6rXL2m-eCU+B z8p-Ob8bxa*hsVLofT0*z4(ArziC(w>2$U>>0+uI-MZ z%L4S(pP#Fq*t6*61qc?z4foO3(_4e>1uWyhLqgc~dv&fEH3s~5>cVi3hr|?MMzHPBjlV&t9UC7ZlX95p&j^He-?)?vpyY~rc*}oYF}}7liByT?3eGZ% z-e5VHMYn{x*#t!x_PaQt-~N6G$2!yAjfw#(iw$S{RPewB2kC3G;9%0K*kP6I7kXLY&#%%{B+3 zhfoi>c0TEI&-7+tomv}+#|Ue&Z3-l}16wIz<@4!yi0HHH#}uDd!JQr1&9rwblXYiE zH~0O4hhtA;!rhBd#=t=X9NZAU_gXI#SgqpJIjjK}3J_=G}8#Qh}tPXWOViY9AaWY=LD72oEkiQwdLDPH!v zbb0MQ492MPd0M)$ymM9Z7V`J!Q{W`j!O^n;#I#Uh7(iuq&V9Lf_)ch;`}T`5hi)~k z#MI~9^@IdZ2Y3U-TUv7#rgBEA4Tc3q&-6^1G?=dcgN}*t{T?Sm1;HlnDUSk4i6Y$h zO5VZl!!+?&@4eGv-5+uf@3BV$`NZ&y*w4OF0N_B}?)5q$t>Q}M{G8#Na290}Y|9k? zQ}h;6izQOT8d=#{$~FavW~smXCkX@mK7@vTDu!rFGGL7FrkM=T6>U5ybb;w=2N@^~ z*;Wfwqw~?FnMQ2u^N_e>q|%cP85k&i=nTL8efjfgAZNoCe^;o+T%{)OYHe!!mz5rEkJPq_3IibXD5Eqr2T@@Q8699S<6WEK!l?`!wz@o z?Z@{qhZ#^s8Af;A_1}McF@o zp>sqv-BP0xxmVDR@L@Y3PklX^c(%IF0OzVvf9}a+1w!=_C3Ef-iLJY5fzh z5~$`t3G#!ld~+g0S%dS)BgqjkW5nPxm+9CWgfPw(e(c$M47eafF}IA770 ze+Ii?SADv)>BidE9@CmwPAy=Z6SjbvoXx}A4>|m>7 zGGD6)NqrCldFhyH)@>JL6EtlEY7^JPlp1cw-t$4RVxy<)Sj9Rgex+6h@OQmXome6BldSgX%$GyfuFbbpIF-|m z5oV!;Yfz}@`a8Q!2;O!vh4YX&%wSG94XKSfaM1X|aD^B{2&|TJGnu@R@XFQwK5(T! zjCv2O>K*ebiW58n-Arjyp3huT=fh=lU~2A*+XH#=O0bWWLzJJOC&3GX`yG|V#Yv`> z>|A|uA9fCz9&{!eYGhId>$b+;>un+y$8%&s)bx`Wd?H!9D<^gqm`MNiCJ#-Bjf9m8 z88b(Y=<-_)7GTl#nob_BJog7qi-oz1x>c=moJ5&4yTIoXYqB}2qi)sda4NAW7pzcP z8Z0Mbg>-uKQ)Cwb=^%D8X_*HiYx+aDJ6JaudCo=L<>lQWG2tz~2oGnO#lu^s7==xZ z+KI_WXu?i3M;%<)cSOh=0`dLRWU52u&xmv?p0fmiz4WGp2)kcbFt9jwMtubL)dKj( zw?mX>*fy{qlx6mB<5XJVn*>3|9cZ_QF?iOEvdqu6oJpH9!!sC%ae7C;XbHXij>W(G z7B@k>_goWhA8vRQpsIA=O_QdLg8Pvy6g?TBqnB%p(Kure{5<>(r$wT+}?m4B28u{en8wkxS%;((RH&;2v{CuQ#I+c=m6eyR+AFA zD_BN&-H4dU^aavpWpT!&LYgOpR1C9as$J%KKdcKTpM5Qn>cA3+UN_3jMH4(`J^nbh zO#)L}Nm7S>nd4K<>8b#ing2~0)pSA|*@*wF16%^vL3CrP?VNaH9ou$?4SzcftyJ$v zc?w>H;y}2?wsC?Vgm<}x24)s~@Ypb8n&LFJBw>Hgoa(m8Tz*C!B(xR?l1Z89V2U_LBMX%?GtHd5P@RkN~jw#$nuN2 zqVqBm<(D{OxndH2Z|Pf00Xd~-soZlGWEssi$arYlhsVI?i*2)i@DLUTJH!3;2O!d3 z#9=y$7PV;7_k;(cn!3=?c%ylMmJFs>^86XS4n8;t&a6=Hb^V~mA30}W=ZT^f^ZovL z=pRBN{_^`RFSlEquPahjdVSY86qCc5#gJl$mS}f2U@r!FsRLc-NU7Nq>6^z7F*N8W zXzG*pePs$jYF8zeIbMehK`^2;#G#)^vof(Q1){6DYRkYS?-I@1dKAnzH5i0<{0Ut+ z?cV-#H^L0USsHTvG&ku;$0Z}lmK|Ony|1q376u84eYtYtd|{b)FUr0jc9IG%ngX8XHcQ z%zTuiyVudr69)`dsPpKS!~TB$jLqwqB?N&&ZiN2x@K9%f)=bV@*Vv@^Cl= zyCq&|P>tFTQN*s zaTuzPkO2Y&;f;rMP}D))TueO+LrdG{HB~qF3*=o#X`^O0;{ivLI@NVEl_?)(RKDvh zLLTN_^mUalcn8Xr$s!J9k2lHiuj#$4je_{pYWaZNa-Wc25fdd0- zOOsVcNQXZrtLDb@_<1bPtQmyi?U#iJh=9T?6v#{!r590MM_JQ0W#U9@4agNEZs+0l z6fJ@jaRcI!KK}fuE-CbBMX@^$SYE*-+YXEF^_q`{hxg^^+vIXs72b zPmoWpS76!a5T`qHn}5_rvkGU{bJZmZnU|F4L9uMbeuB#82*0po{tbUAeI6y%FX?XZ zGJAJ4gar+n^+2^&Qdb8wpA2g~Jvu_Md3C)_b^R^y+C@rH0+Qv|2;a!$%V>+{!4qvn z@5S(NNEbe1@`9B)E#Ev1+z`H@)ieULW-j&UD8Kkh@-OK4@Qb`?#Jw!Dq7$iT;7*Aa zF7ehc2)yZu7jhTHJ3L$F*$!^6a&KDPEjX>kc!)sDMtBT95tMVWMYwwP+FPhES UI(V$&;5TQi%xq07PvUO;59%x08~^|S literal 0 HcmV?d00001 diff --git a/assets/icons/pm_dark_yellow_128.png b/assets/icons/pm_dark_yellow_128.png new file mode 100644 index 0000000000000000000000000000000000000000..d16ff38e8ec6a5f9a1f7d06882ce1039354677cd GIT binary patch literal 11569 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 literal 0 HcmV?d00001 diff --git a/assets/icons/pm_dark_yellow_256.png b/assets/icons/pm_dark_yellow_256.png new file mode 100644 index 0000000000000000000000000000000000000000..9c98b26f726f0e78341b4a4daae6df7efd878f58 GIT binary patch literal 18137 zcmcG#byQr>wl2D8Xx!Z)xI^O*+zFl#Ab8^hcMH%!a1WN?5&{YC5Ui2l0TSHZ-5nmk zbN1f%jyLYRNUt*^c*HEUL_s_IBhbww<6a&!Oyu#}bLUIPHaa}fbRMSlJ< zbS<_50Q5y$Sy@dvTQ_HWs1E=rMW&>AX(sIu_v9R@Ff!C4B~$wnd{x4elVJu0yW^%L zjK1i>t@I++b>=KCHhot`o4>q^-lva?QC~-YDUbKd)oA=z*uX=G%JJd+mzRE4i#~y8 zLq0iY*dzTsW!>*E1PXWKDA z-hmiMzR9o$wh+ z(-%1AMZUoRjNd;qOHBaqq+YojL~S=%KXK$b88C+jv}>9W#K0dR=6>eX^Ze1 z283x&Kw)D;Ee+V$1xhh~k@3R15SJ5gv#fgpZ;IF+%B_Gx0C}{Wfi{&coB!o9C_9) zu!?;W;jP}nD3L_?N$pfg_U|W3HQ$6b3l=h2^%s17Wb2gG5r)c+3?+U37KlEdT9oRO zves3q<3Uku^5JGORYl&K_pEry46j7x)!#|6<|V)R!Qzo@^@?HxxiIN8>NN6Ik?Z#l zKg1`Z$z@^-qrUSMV-&L&6OD_HBk$VoV(rqu=f0qbmF3Hw`6Fg4qYvT?5lqriWiLAT zM)fT#uWsz8?a_|=Pwa)LLv@@y=D#&NZaeJT2HO|s#J2*zB(3osrrW&xq>C}AVG((z zgnWXKONcs8Ip0q*nlC?8&A;*SMSST_=1t=n{Nnee_si6mkTlmpXnU0IGE8tx$p4^ti&LEa z#{cVWWZAEJ%e$JS8Ss9}{(Dc1JM25E@UE(LRAFUGur*^*m|N6Y!kJcV$!0E-c2Y5? zc8Ru~W^?(azLETb%!01}k6X$M*9+NQ-EkA!VjI2p*LD&7#5UURDgSJbZjY_M7qwNe z*|aSmoiFh*u2GE6v;0m}%=m}x?`)pOtCDDym2{VfCbPrWhw6u9jzaOnHLGYoGcB(d z0~RffE9z4(Ne_e%-p!`W_SU}iU=d9fmB}*9dMk?J;kfgG8YBk5R!~@tsus59<;kWv6e8I<0)Z73V&;#V`9Pe}!8#_gf|fykQ^H8)N)-@eOB4&x+b=$7*j#|F`e&*xy~h zx4ulJFV$&Q9ar|%PMiL*I_da3z`N-B#j?OMenN>oZ-TN!zvOO`(vD`2Z1U}-ki)hh zwV;TgfqnVxr~2~xFZC()y8B8mg_=27SW)~WtTVreQMF2J?DD}d%+{9)^^0Z9`SjQ9$~h4LQUgRFxjfys+C zfcgV#7t0eXn_?bc1_KT29l_rh{De`V0-cL!hXk7%!c+s;vDjs({*+b}OgMJf%w5bM zE3n!n|Cr^wo0%igeZ;e)r{&hfEqaZVXQ@&2#t)NK$C^mp*jiiQc z)40^bUfSL+)7Q||P;EZ^8g^ZMJ^b+x+8F^C(=MYGqaY*X)9NQr<_k6K%$5LGuWCD2 zm(wyN-XD&ekBt{aexrJoSJ+*&uXaNhh5I)0_I2p@j5mJ29#^qe)9`~V zvetdlz4YJB*0>*?SD$BN9>vs=jqv&N4)U2;-dMl2Xzj=DUrc>?!Aw^qYE3I7(I653 zCGC@!+z-aziP_nX{Qi^YcDyg&Rh7DhZN*&h`EKN$F5JjHCf_rGHD0_iWUJ)5&Rf=6 zx$Ye3jOSakqE0UqQ4!tyYd<$LXf2sRz;8WexNCSaKk37CMm*xjzvT6J3GNOa2bW>| zCMM?GHzsVc88Mq&&HUBQ?PB=4e!ZgbL?t5{Gn$yl;{o?x)}czjs-@BV%T%?4W~4RuL}ds|b>+kwH7c^wBMH4{!Ff4kU`baBgc zOSgfalVKGOdc7vP)rS=qW0N`DG;i7*5bb0;!iHm^6d8hFob+3FROGGX{R&MiAFM)t z4+;fN)6Ka0mzdfu2zEIX&PhCFUYs2qbj(rw;3ygJ8ucPP^!~}`Xg$XjZzRxs;hA>$ zsLxq;I8RWw*hH0XMn!D1=l#!-izFeCV`eLBi%*rm z*il6-lHbNs1#X|*lid5fj;@;rd6jsR75=$-c*Ya9VdlUM%-6x+}k}fCScE%3q3q$hm5JlI>Sqi1(B5`SAXZ^6BQ?(-3zI zH%0sG2Zu-E+k;1`ML0~Afdm0y0s79}mmLJ)%1Dj62$Sm_H{^g6<|6?R!q@k-*+E7L zEF+Jj!t{U@kSPYBf{@Dv(1E35fUnWft`E$oV@8=Mt=bd_`ZsWRvo{Qi_~C$*blZuN z^!g`7?mSq35oh=USRO?F><@dYy+rW87e}^!Jo3H2!%pDhGC#h0STdO-?F)Nq20u@D z(VUguxd8wM!9QOFAU%^50Kf;f+IsGKYO122w9uc+{L_Ep2R-d|fTy_^NB0``Vd{SkOsI(29ABJ`*@t zx(@Fm$kXBDklUCNr)sj|#OOVqX!Y@oK zB*Fy|EvN7=S7F(HZxbAx|3mBO_AfU*JB-^K>dXz{;`zsHWw1)1Zv?MXj7%9iY#VV(S34w&Zqpw5Fr|U)snz**m$a zIaxgW{8{S%y085FUhC>)Wo!TZ;9u{z|AhOq@Mj~HY~7yQ*5_Xx=#8bzze@JDwEq>A zqEPdHI)gZ!`9D!$X+ig|+qVA)=l;*k_-|S-8_Q?X|4VHAH_Xk+%H0dTH8DGAj>P0((<8q$)J2)|90b( z6#fsitkIW$KLW46hQJ8%zLpSTv8b`WN@M~dNV9^_XiW>#a#&s`bNwX%eSIFa5AGkY z_nFcLQ~a%RW~@EE7AvOA%^JV>IC^dJjhvN1M$RH4B3RTu0abcK7h-h#JebAc_b|3x zY|?D%6sdQ&Bj~j1w|6K%g9t~_Y7vcgB-22#JCX)Lo98zDz1#$N9NAl(%?Nbp;ljRs zy$D!vHhA~>XCMYy;cMVGHt`>Sx~NA^$*J)}b3B_oljLd03*O#7Bb}tilOR-ApOY;Tp1oS7zVH zyivT#yb-Qn>I=9xWoonHqe(rIVcVncBiW~V&qoNnvp)tS#feAEHQBWQqf8ne_-o+~@YAbTQT%HurA71gjBw#l)Fh`>z}ie7Z$ zXKxigF?S-Tz`5XR@bJDAVk|#%>SF^_nsutkzCIyg8s)llzeSPj9Kdaa2-=TWI2$jI+Zj3G3BAtqfM!>F>)J54B7TzwB9pO zwKaz`@-gwD)42_%#3D)nrt0BUMvR^U10XvFWGY&Wvv;3}9J^T*L~e&G0m$E|g=s>o zezqkRB+gyqqjkKOE0tmZ*!d{0ssB*4llE@GLs@v(gOvc^H`tURywiqD#eYyeKOki) ztT5@lqsIrTFpe<~orY}Iy;{&egy4T02b*Fo3u?nVFMg9^d6^4V;xQ_HN0$P-?)b;u zWpI zsPOvYzK?qp?Bt;Wi{Q&|5v-mFEGSm^trrC=f$j*pd6WtH1)ssDY{%B~OHzMW;!bc( zF`{D3h+thXZs));xq`+X{AG)%2r~j06h6_|u9dGfzou@=ATI?$gX5AxXw2%4 z$UJIL3L<;6rx{(1S4=spG@ek4KpKkvO|Gy>cimQ-`&71JtS?)J5H_L7xL9VS(fwk9 zrRYI+S#7z!IIe>k4l-pOB^#|99L;d#fA7glr1$b+YquENgjILTA4Qmu3{6%e4-KvW zZ`}@KXCyA5*eg8uCKBI|rX2n*8Ah))jXRC`uD>XsNRIVHgcn<4#g|T2OGyys0BdSo z+S@R9F9^T)U8b$wRbIbG+sq+-@Ac@h{3v6$1an3WJ=8p|WvB1UOwd9u4IglMKr6F13_O&M^IUdj&oPuD3Vz~o>(vZc^ zh27-EiM>lOo%ui-=@>@~QcOlD3Cw>q5Bt1?9xpwiHbkv}60w}KT3NrJe6jX_Fs@wed*FHu; z+uQwR290JU4;9#tNX5z;$2oaIgGI5E1$gqVqTX@F>9zr`|W{nq`VbyM^w z4Dld9>=7*kun(h~HVjm5iUGqnQW5X-#w=2rzB6D=do2Z{tJ@;S@i zUPYj$M#41`r++>!KE#E7q)iv#0P$d_n0r1HUQ@Xx$V<|-tAefaDy^rkU^ypRw_eT) zq)55w*BFQGn38Km`5(XXR1zF>93G)at}U@t4WN~SX|dsyiSd{ zuta#E^ApsX?2gO*!KbW%!5h-=t-5b!1rA;XouLIXt`qu1 zK>E`F!p^pKEMdlC99lYeO1W#<$gMn?lAuIxghucc&8DfjM5V52yT6>?4CtEX@Tk3` zr+}r875DT7fU^21OKE?P)5J*sDya?@xbi4LX`e|w&P`Jbl)h?YziA6|gwc{>57DyS zyhV+X4Kta#T9YzIL->`4#0hwT~Ld5AF4OBp$4f zOg~>;OTjpvMdocb&V>aomWz+6)YB8|UnfzIx6$y0@8SGl)AwmX){ z#IISq*6b3z#cXUP@NS?+7Ry*;tqYj;q4pYWRZXC4iIrQJl)$RJ_`MxSC@(yabrVOj zkNTKu-7SuVa#6}NJFnjN<|{Izz(x!ff*#3redyWrI%rv4Wos5!{C+Pte2JUN=7zWl zj1kWa<1^C8$GIyYd96N;3!`}fKBDt=um`x}dr~0gDjFTJUvw;hFq;n%kfEv>f{|!;rTA zx4B9z#?~y_wMg}OgE{v#T^Octb3QkBex(gcoNV1)XJ^y8W;YWF!ZfvI zOK7MYEKpx~#1n3*G%Z?1>XX7m;OoO1M=m3`-i-oR=6Iqus$s20xuzpQcy`H&-BQTq zZc{a@Gx+uRBi)_-A7pzN+n5|7b)Xb`ccn9vgf2qB8c;8-6$r(J;b{Ht7;zOF;UCYl zr=fW{SCzP<@owx1fi|(gt9kpsfhJz2lEvr=hYJ$CDwe98Hf%&$5Xqk zXzW5R!O!PS5~3=2IfF&*uK-SdK2}pDRW(g5ex^#VY)PSOt&s?V_sd*6`NlO7Le}kc z-evD+cncuFlC$7JYDb`7-TtLZgb8SueitK6_8?|qq8FTc-!#ic*Qti6zXE=1FK9rV z&(Km};S&DPh2zZGBz})pdz2$rN&FmrE@iswRRVx-1D?E!iv`rtxaO`krdaaSS=% z%!%CvBOknrKxGb;Bl>8FPd~VtH-jogez_?@306TOYI=(z1`IJwiv>qG zCxHUc&xNylv-@~NSZ1E*PBuIHpVBnbsM=L!dKz2KKU$0P z&f~li1XY?Gy7hl4>Ej$-CDb10iwLhFW>omiK-ocycX$FbSq>oLu}DAG1n~rOXfv2T zhL8*0S{j&o?Q=hf6H=Tw5vGtPLm~M*KE!qTZvB_V*c&j+ls^sP=uaN~Sl5A#16qeN zpu4+MC=14k{l4}KHj!_o=@vu9!s4xqNl8phUwB!SfApHqak4e7wGlr)=rHFDaW?kt z5JtV5`MpB9J34q278GDwtOU<+HaNMWHx+%PseDM3?(xjmP!QITdFPM!u5w3#h(9LM zZCzTx-K&duY>-SyuqqhnHX{d48Xr$Lf~f<9nnN=V?N`@2@q>BCqXPKC-Mlf$kaRoG zTBO0b#NH_{QgIR7Adh^H?=_321S;+Bqw_{Ki{J;uBL>O)b#_FG)Y@te9+?ge#c&mA zy1b{!$-}baV;nhNvI1YdnD{Nvvz&(J>m7mE`@JQwxuXbmgAYhW(-0o+|J0=ks>C>+ zk}47u{tag;Zpfj*R@CvCd%PCa#@|Jm{w`st&46Fok4&`RI>%GLSo?|dTN**Mv_?F& zce^)rGGpF1(P2fmW}kx#b*qK3A(|Z2)*VZJ-^8Coc-O=aEu~@ju)#lShf#Ns2-`zh zEN3AeaI@djt8u4e)Mb_EEl%Y<7G!b&yktAOoKu<*jv* zcaNi;OD5PJ+rLuaWY`sfB)%`o;immkmc?9cDu;b5(`4}3Y5!}!oQaX+t79jP>+~Xh z>x+I%cTeBiTHi^UfCT^CW{PwJF=B3ImwP+`u`^RdNy$J40|)#tQGtikm-9a(8Z2R{ z?ugIxZuqVAZzhaIX)wuyX3&p=*?QuM+_(L2B;vB-QlY&Cpl|Xjx@l=szB3L7_`w8m zr5j&RC>lR(_K6vb+ZiJ~j;vfTL!|N&f{;up=6fDTI@&h^UJ^Fhj2-I+#js4>2zz=g z>sfFgYlbcBmi^`c!6pUKI=te3Yqx|_=inYQ_Gtaz1N6n{E)iF26nZsE^ zmtDL}eKFCt1|ZmLG#1ZQt9>fV$)<01ckm=MRMJu!>dTBNm`12iwjciU=k%ep-f6el z=mckccYTJg>(X!YFI6>To-rD$g7zO6xwQj9%e?j{oXPiV;hc#}OFBI!l&!5!d}8IG z8^b{|--!enA8fy>c`jT$AXeoy|2VVHwa|s3YaE|OM{nc73!W?q?({a0W|+a%(%SXa z!co-aLVZl&+nt-wljn0(4tdoN;$jT^e5e{D;3W%!*}Y&rRjvW~SXOnB@>J$SC)2FD zEPFIV@_;E>#=X=(x8ahc|XLa-NEjLMiJ+DA*@#oXbWqKUp5vfH>994CYR zqdcyl)RcL@F+GL2h%qYkin(QeE@cSRFgV#{x4Q0euk!}RNu=R{kNlOcO-|e z`pSZg)8SDd9!fpVqcJ>;M2t}!io8=pK@JU<^W}_5>1i|{&1(NU>+BRiSYg-{I=J5= zX3#3mtUE%O3S7NU6QhTjZeT(3nXe=pV}u-UeM3TuOPWMgsz=IH9^o@6=J%ET-kcSaE1r{czo`zAKe4+^Ds&I5d{M*nDNFKDejJ@e09a39cc$$8d<3z}vU{<2W-nhMw zSK9j1V{O?fXwRjFNH2A7%nQ66O!#>9^yUwu<=We1IgzI$Qc6CoI+FoUj89Tk%NnG= zIT&Kd@wVbj;_}|>#gyZ32d*QSaH&=;QBR27z=g~2`&lMGn>h#^v%aT&N#(H^0P@o+ zm4&dLrSX&kskz$9k{^DpiVbKc;SfkZ_Izekf1BGH*l~fU#IPK7rT}?w(PH}HK``Q1 z?srCcOZk{L@g*A_bGtK6VQ;xRWX8`utqDhXF~kX^c@Y+l29a_xFYw(utglO|NED4R z436k>vsi71JMU=+LRJ|Ps`cqxqlT(lVjU+hb z!?5R#Nt_s_TdWl^NC7+kY%eGNTPuC2uJ6bbuNh=}``qTea@7g)hL}Ke(AhAh#Cg!> zJ&8x=EapW{r2{v4>@Fe)0SvD%|1kLJM$Azxy#WhBDIaC5o0gBL=6r$lF(fNfOMs&% zg(SvoJi=Rf|E{LvEVuAiK?_IEyCtgOF*ow-p6nnb9DmR~>NKbk+3$ffJkJ5ni%{Ha zgg1KSPc4M~d#2|lYvexejDq#A1V#aa6XksWb`$d=KJ}MvaRoT#to?;gn(iJY=QrmU zgH2ny=1sSq&;4BlH*F;xzEfP=Vr%>JF(PsC)EdbY^Uw^~yc2Wey~KyD+RUVI3VeR| zlw?ylW+TYi5K|oOwx1`!;fQJbDG>FzWkUc}L*!ekNX@Q@=@DEe(nNu*=x(ju(JG9n z9w=}V`(h$?Pa)a^EsD{>6z`*X4ln9Zb?ZBe?$O6~(}-Wo0~iW_G;?xGfWLlS@U-aH zs>Y1{MnW+gyV0QET$v$1?v0h61ej}}3>+kYt^go<%ng`V);z_((C8EkF=zVZSzzJw zh&`l3I(#!5*Cp(o^TTGxNvcPwQMi@ddt2z@!>_g?T3L)^^L2^ywRieZ@bW8Q_Baib zK(-zB8!0ywxp$n;`Ate2gQoa(@D8J}r+Y93H-(yYk?voeC&O!3Xq0jZgU@9La)9@) z^XkkZ=jJ^XW5MrkjxftmB3KDqB4*!vjJ!{V&<`Bad{T4cA(0#11r-UrwrQJ zh?JE$u_RPQTHD~qKmZvQ_YfWt7n5K?d||`*0quw4%@IdD8N#71P+AxXe^N z@}!ZA!#(F^t^lA1I|x=RkFV|8yk{Mj%lR-iaqwsIu_XXE=Wls`>85@e25wBHBh3TF@@!QV3H zV&98*r+j|9Ocq0GRi2DqO#)Np>tUif8(H_R*yhD#W>$&$fnK!Y6ob%?MAn&_~|PRo4r~V@tsl`+m%7V61Bp1iQof7^QOi+)mJ_++lNqyiw1!88g2DsZMFZbpq;eMN@{~(R zYiM*}z->%rwS~>i$5$QkAJ+_+aE^}_SSr@2wEJ%v38SA75PW>aaELint1ibq8QZy6`uZ5V zD<)V(-O~i(*F=s*h&B|>F20#z0|O6a-nigZJ-;T;D=9a-sOS;u4g^0Pm(hlH)5H6D zPr5G|({I1(D*&pt3N)p$*P$`MIOau&t?wu=&z#*=?yhv6S$G-S_tF?5nQ;@5N+EO$ zAEHKd`G(Mym7{oN_zAqA7iEQXS(Wq=P3EGA_Btft5UW*Scc7`O%$XL84KESBH$~>oaahdRf zGF~pUrcW?4@9bh10^^{f+(jRug1^(D>YT@%>%JMkUGIXNcs&tD&Km7Em6qt1eK9nH z0@r?n_OGW}&&8NXDbLVmwjdV6?LC^3pK(5+O*`~L-Sv2A-+jYuJu#8{VsF=+rB2%& z^Z+Y@H77REG5_?-Efn}=$*1Vr(-8hfU}%UV6%Wwe`2o4dfLl+ajrSq#sX~OUB=CRR zgZpH4?6zX4Q%s{{2X~H>K2lMd$WESRp!IJN@}Mo_*0$b|i4bq76*7QGa`=MVAKHS@ zux8px_&}9_*9v^ipZW?-KWy$4>GL_6y$mSXfl`Kq7YY1Vq32^oU|E!lIe~k4*Oer- z_<5k5#Qp06Vqugzq?Zh71eP%&6lJ=@neQ&%MN26FZEzDyYKqdsv&I{nVL_x^3UHu> zUBbC$UZ{PcpgD^3t)~iu_CZ4dA0f_Dkd4L%$``{aeFS!SoE=8RtF=z4f`xj4??q~? zC-@*3czvi+^oR8b0pMjmekTe~nB%!%I{!G%%Cc~C6Bx-1A>-I5_Djr#&_ z6FSn*R3t&4=ZRGP0IpFTz&2&4JckySM!0MT45xoEM!FAk=e&(5ouS$wv0uGH61Npv z+1QF*Uw10w0JVbQP8o-Wkp;DCk}a{+Y!nF6MoJN?o2SG-ZgLJ@m!WKc_bvGORD>&Q z-vSN!&eN!YrC4HNhJ|ej8Kch%DV{7y(2d@8+>9TAIhMWNT~p8goUTOts@Yf`nO9C! z22wqJcnPC&!6o~=1Mb%t4Sp7Y zb0SmgT4jl{t>EXL)YJld=V^*FdwE@dP7D+T-33)NXDi`pf#;>MgJ+Y1Cw1c4hbex2 z-x*z56gmiRv@>mDdgR+Q?l88@jVWC0HhF>!pa{JK*U}qELCCumfGdP%(QHM?YwUaS z4WjFO=ECIF=wWJvQWijkv@u#~GzNL{#AlyC|-E5WFAr&b~8a2l}aC z5v>%R(!yEwbL0z^r^9mQ*P!Hr^VLkry*8)c_68Uz9i9^Lb}}V^0hmry%RN7`eNi@5 zKj|${T-<-T-p`I}N+YV77w3fJ6BI#2iaZ~=Z{4=T4mfSNR#VR4K1E7%n{q=~YQ&aC zjz2hoo;o?=;IfCkAt&wNfhcWsiMy5zZyxd7w(tSy^Ljj_Z*28tVD8buF^%@=G|$IhviXZp^4G4z%(2=ZtP7E`lXE|I@%RT zj=+e?m;_n9BnDiT6)2g6YS;sE$kYzrhA&-1 zJ)no__KG;;WJsDa_KWw9`g7t2RIorP?3M3$lghvp)COn!2-zRcD=T>5_-NKfp>ERN z0a2<1W&|0;M{+2jJ`qXtOs4V)i{HZ@2nl?@mgFPOKp?^n?}5j2sf^L>x#+25~s>d}9;n$Gf>PNF#lyEV%)H`FH9iUMQx)jsGV!7VeQI5-I$P zFIRJ(>~wnA2-tm>;dcZu!FfkSUL&3RW2W@x-*2TbfA@aXL+zt>dC6OP8>zLe0CER* z_YSAzf5yaN$Iryqlj_DDbSYoQ&qVa$q$OF##s*y$k4$GoLoEAHj?!j0t~Tx%-wZS( z&k)D;m?VtTX`feP1(u`}&W70o_qZ3GXAW!I${pc3o=N9N99bXrRTMvFOu*b~ZaRXH zmPO`?(7@9<`vx8t8@B8H$2I7E$*44O1F^s59(~3-XFgJ?&GxZ>646xgx@*YasGmUGkwZQ)$VA}+)%6xV!Li5ps(R@7>$w5LuO za&0G1`7S|BWKnCxc%!U+*%UL+OTtH#@c!h5`oiPJzI{F{N59!jJk@Yy@hx66^bNq($>u9fGNZ3v2KbZ&V^FvknTy(SCy7y z=moD9TBFK@%8tiKjxqO}bm&h9jae!RO^^v-Ozt&5*E5PEzR{#7P?T5v!x4y`qWd%X zUP#`DSl&Uv+_6+Oe^(7d6+5bTCE0v$d-qb+4zXO;C5@Zia~1A|au*+R60y)k$`?tA z{v!XTJ%(c&t=1_!+VDLyP_ZG}LFtVp@8BPS#A;9yfO$dB;~O`nUlt=!lzg*;?hf)? zW1~AAd1SG3W_(`UwuZ%*YV^hsoX~hoM$T$lrp`9xZ#+`oH(%JFLeOWu$;(u$PYm5x z3LLu}U+VBWKJNq9luMcSsln(wr3DbZou4@vS(<+{?j;$Rj=F8D`GI_=iDy11%j;`a zj&iJTQgFBn2cj2Rnm5!G z%FBNN-Lzc~7}6%%9g)%t+oh=|&k{k#xtV7H_t1RE_qY z*=TeyTaPjFL zxEQlG5qD(Tl9Zr_aa*wFQNt=>IWZ#GDm3^d6J)1hE-Rdysi3&_^}5RPQD5B`fjOsl z%5>0{`z3_HP*u9lk^1wfJsv!mWFCj~RHh7E;5<6?xhY+y119(zNSmMN)=W-LiPrIe zA*5Wgx&T)-&L6TzxeNX)xbYoi%;S)JE_ICoC14=vHz-}_qaXhH`!4JBRA1&5)UPw) zt2^IGP*MC_(+S$%$*#R#gY~{W$q6OXBIT$#`iu_m?GqqE<^t~;Y0#BE{|6%L`-Ws! z&w9hClHB7=g8PTaoK8X^TCZfgZ_70luOI~RUC&d_>c?7MNuMXvMO4Q8w2XeV(PKZkowsM&Yw({(i3cwHM@eLTtAQqXCI z)yetQKNFVoXx@Zcu(#iN@Lfq@?FE};P^i&(O+R(=~%LvzHjxVM;5xx$4sap!sg^sm5D^LRGF`RXHRnZwN164Ema02& zs{5c$p{nR{(epDKHA+_)bQbeUGFDElUm<}mDuk<&8vrCb{9jf*PGkR?ZN?@P1@sWl z8XP3(&eOvl6C>#Iz`=MhZcVHwp3}F4jq5)aCTzwtM$gqK=q1JRP8S$OI;o;xkRkw_ z4M*79^G((C@4YtZ_6~5zPZ|f08%ag1P_6n`#SH1`DEVcaL4ZE^Bc~v3mkvuJYfjx! zPl0Gy0C{P5!U3&~1hW`fHeSamBN98111duCel&bXGr>R2k24j8xTN5yEcyJWHHp=8 zcJ>&=pv}abgf-Nwp-@vWP`lK|xNNuHxog8%=>?yPq-PIc_f5)RV*dppgk@95_vSbh z<#)2s7o%Mdt-~UMHwaLT0U!Wq$qwJUE~K8}P{%qZUx2HQ&mH3tPUxy!c8F7d10@8L zm>lNL5BW^13yTZmUETnv^gyi5&D^JOyPHD%36YjC)3O6@#3fl7XQ6@oQWcZHN?i|} zfpNYlPOBOMFCLTaG+*OHxy1(yrIvi zPn29$O?saDt+eZ@NAN_ziAMxqDdlZDDRW(|AqJ@unPSv`)*B2qP@@vTCloOzposJ|_ z-`&B6FO4F8IU3pHTOKLqJx17Rw~ielf+wA*C*Anb(c3WR8uGU?F@AqLV=I0s(AHz8PJ6cTnABGzQ~|xGi3W=h34l zJt@iEgMuBdB6kA(8hCTA@83)aBjiPYoLHf}=b`jC@}ZtyIx$3M2t&#(=$cY5K|?c= zbNPcPHhD*5hz_vuU!tOqUDUq#!GBI$*);dPbf~ckt^TvIbodjwWMHLmR)q60@98N= zR~-PmtV7XiR2lXk&AvgF8Q1r_<76i_$;mD1Z791yBjmx?5))I+5lx0Z+vTMt8$S|$ z+mY}(RE&ZWnL0`n)QW8=0G zF$!Qu1*x{Wc{-Nr+lQCJyW`-Ny8x0DE3%;0rsz(WDFw`70{Bb+Cy45^Q`me_LrpRq zF{3D+&kJMyQ2c!LE#v7SHyR`GkCI2qk;)Uz3i}21O8nfm;!qb8kZYPIi{^D{T0fq5 z`=pU)nqSc8hB%Bu?Xc0a#1FC3?i|sfmA{^ClmC21{KKybA4&*}THw$dswuj?z~28j z`{9Y;8e9qX*){vHp{C-qwxAyN_EsNlNMR-lFLY21Q*(!P(}x<8BfaKhs!@ycTT#5_ z!&q#$DK>sf1FCa->t-@TV4ypws&OAN>;uk4U<`}c=8Lq8;I^l{fYx}~W>DWTOH>f| z$MqL)J{1e^Y$%~-%gn$s@TIgqT}kY3Mp2|)3!pA4to*c9$=ciAS{cE;B$5hq&{UX5 znj+{&6lXEH16vS2p!`^}`m5+HRo-gB&rD@Bn1236%@7WnuN8t=lFmIog(x zeLe-kXaIIQn-!GWrSOX{`r{anJr%M(*zk0aF*)tm{a1El#dTs#!~;0wdV5P#6_l{z zs6BAUUC#W;x*oVZbz4qp*B>}&cluU7Id$NS-b$_}C%mFcDDqwvf({g)tW0T1cA6=% z52x0ge1UpDpMtQpJyZHjvkI(*@_5Ruu0-uUOJ;};u{i?5UhJRiFwH?oiXR21WL z^x(+V^fU4F^mEQ%MyG=5nAiAoJkB&fq&70lh+&u`1$}kVEU`f9m+Sp^&d-tGbk;_! z5P$qu@z_WE_-ofl9es=sNSGLGee;d*c`jomN+WRT-E(3XY$d+@G61LT{+6-jR)M5W zpiLBA95x@K!%ISsyzZl|{BrBBPaz#ZW(;`BzIz?2A0$jMjI49wwyc=rS+ms8ww!hK z^FasqC!%nq5>oK0JjY}oi@@_xt5tzek8GutDYUD%tQdgvXkjdrX4L-id8>Z)@h~Cd z&nf>b_Vk9W#eBBY-p4`~fNW7wXy=e{tA1I})Sw+@>3Q`*ve6l`vC+1QD8(Ms6;v3o zg%bN3lhT~a%h0zok z;yf8qqvYXY1h0ow!O{BiTYs!It-r#k6lY`HjE-C*7-}lZ_L$--_&&o|#`rl8d%{_G z;7IIM%ni$cYoVucJMcn7O4vc71ij4O>v1m6Ta?Pp?Wt>I z>e3LhE;kY7#50CtBW*Sb9`ri8|0fg!>-t2j ze*~Rfo?q`kZ{ol=Q7yZvIiIaCNTCWoKLy@(F@Z<`>{5ZY1qY%%?rLtQGbq=if~q-j z%avm|`k)#XEHdsgG!S*j^YUE^+vqwJe?%?eo^m;fNC1Lk6k5{mgNl>*ipvj}Q&2@8 z-%MU5$?tzXmi4BV)6|@@B?16rP=%XgD4tGG-RQx;j*t|qi3A`&)FVIq475TBOKYu8 zWYFxlZ+x~)9G_s$ta{d)TF%lYt0Dk0x*mvxpisL(eTh0i-;CU`p&=50{4*Tc3BHO% zVFT3Z$Q28LJ;*UiZ{qj_vu~|u{i)?FwYw6?kU+^>ha({kdvI0{O4mPfJ6ciE|TcYwhme)Y=X86lki#L$AEW{2;}4*oY;_L_N=k2`;{`9TYQHM zn=k@(jot+b>BcCGJ0Elb|3w8+pGE%r5767cAczDY{8S@RSQD)drl$Ws0o~{}!)fQD z2(&kHImyHZrX3pApi?dZHG*KhkEpR)uYd-0T_)wzdDpMU6KB?2gOFTAlt~o^!Yvo=Ak_Nx#So> l5jJ9;g(!}Qh={`E{{gA>oRgaHVVM8`002ovPDHLkV1iLwY{LKm literal 0 HcmV?d00001 diff --git a/assets/icons/pm_dark_yellow_512.ico b/assets/icons/pm_dark_yellow_512.ico new file mode 100644 index 0000000000000000000000000000000000000000..a047a14c8d03fcdf316d74d419033ecc7c2df698 GIT binary patch literal 112046 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 N002ovPDHLkV1f%XypI3? literal 0 HcmV?d00001 diff --git a/assets/icons/pm_dark_yellow_512.png b/assets/icons/pm_dark_yellow_512.png new file mode 100644 index 0000000000000000000000000000000000000000..dc5697a2e5b883341c610748f44e119d2adcbd2f GIT binary patch literal 28968 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 literal 0 HcmV?d00001 diff --git a/assets/icons/pm_light_128.png b/assets/icons/pm_light_128.png new file mode 100644 index 0000000000000000000000000000000000000000..063948f1789284627332181127d006f59e99d9b3 GIT binary patch literal 11328 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 literal 0 HcmV?d00001 diff --git a/assets/icons/pm_light_256.png b/assets/icons/pm_light_256.png new file mode 100644 index 0000000000000000000000000000000000000000..681b0c0a198b2e49140095af8cd0ff705a5d6d63 GIT binary patch literal 17563 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 aN+~RSfIeOLsqpXBd6s6jrj^E?iT?v^;&uiA literal 0 HcmV?d00001 diff --git a/assets/icons/pm_light_blue_128.png b/assets/icons/pm_light_blue_128.png new file mode 100644 index 0000000000000000000000000000000000000000..c088b3cd6d4eb57dbd02694bc83d802e878a14ac GIT binary patch literal 10888 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 SSAPDHZg|>E_p!E9*#80;tfV9W literal 0 HcmV?d00001 diff --git a/assets/icons/pm_light_blue_256.png b/assets/icons/pm_light_blue_256.png new file mode 100644 index 0000000000000000000000000000000000000000..616c93361b375d82ca13bec18104b82a4bde7469 GIT binary patch literal 15813 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 literal 0 HcmV?d00001 diff --git a/assets/icons/pm_light_blue_512.ico b/assets/icons/pm_light_blue_512.ico new file mode 100644 index 0000000000000000000000000000000000000000..72ba27478072a423f38de0f5d54fc6df305e22b3 GIT binary patch literal 113172 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�aC2PooL>4phx1w;47K;4oB_Boi&bPBMeqnthK@IK(IaL~Z6`*Vk+c z0Z_pTfi7ecEJmjUt+G3f0^Wv_jWWRsfvxCN;8I#k;O2!Vf&V~BWsz0?3QZ*dV1PET z6h%S3AC(_(g~@q1a1vza3$PRJY2XutwBf8-k6Ow78saa0XhH!{!3u%Rs65weQ3TsY z-{Wd9BhiV7k5Ju5kifmU-VX{yVl(g+G{5_jEPiNW0RRKEfd_zp1Ky2%1I-*kcpy3v z$*gIKGx`3G%AbX5!i{AMa4w4W>rA@%p&-dXY2Yk|`RKIZ%`|Uj;X|l}+268E8@35U zpbkpsBViOsm(3{lCa+HhzGrDK)I4@nASY;6M~C zbhz&u(L_}0V|)wPT<_wC78C$5KpU8doM!Jq6SkSU;jTlu`&U4=1wP_%i)@8*UAutyOXPrupD^TT} zN6sbrLychHjp{rGqH`xI zhj%St(p>yd{0W{aPUWG!fj1&Y+FXi#pH?GN;CJAs+OQP@qmh>NX>?Bm!_$S*dOw5B z>rZ>}L$N1z!?1#&0||n~sOje}6xTGk*kv4D&@H~p=EHtQ~=SP68o`nFwjwmkU3}A}yo0B8@P)YbNpk5I@ zjav8>f&hqs6$EWab$vM!1P4;AUIiP`vEUN$+jztZf|2M5_AKBKf`jlG6xDYgHpRNp ziywjjh=mmdmF_+-M5e)uQ8A7pmx2VNdC2g>(F9bnTPgcRuHrU2LSH{PNX%OkMyF@>F)x+0M1z7 z_ftRKi%LG8ev{xh4dguAB)0kQQ|$I*L6SnOyl8WOBOD zao(k9$6rnzS$t_x^qZPX91~$A5(bNaqflp^`6w91_q|^Wd>A>NOkRWl3=jmPk+bzQ zWMa*uSSK(B%7X5EgEk$oTel_`d>P?Cx3_5@vfPIiwa4_;8%)p;^@EFR2xDs;J zl6*T>7<8Z$m}5~1^aD}-!@R~3Y(*KF*P&t^cOXHKxs6|o{5|m|$MG4AL#Dw#XhQFY z;ti$&W618=EJZ@#ddQ~`fB{;;cod8A5~NivME;i%9*1YpG3yG{HR(EZ(z1#S&hqOa zpHI7maAFL`0kct;pM6j!-7d(4n1s~fMjbWYhvM}A0{ImJ6`ZKTarXWkjLfY4(EXc; zw5mp3NH3~2xeB-+)#1Mt-N$E1Ibo-f1j;1gM?R+y(MWdNWNKB@_=UGOJ|8hKK4yJ`;E{s6MUdAEQ zY9Zu0000< KMNUMnLSTYQmvA`% literal 0 HcmV?d00001 diff --git a/assets/icons/pm_light_blue_512.png b/assets/icons/pm_light_blue_512.png new file mode 100644 index 0000000000000000000000000000000000000000..d7eaa1fc991f3f13f657621e0e5abe4d4f6c0108 GIT binary patch literal 25466 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 v#g4%>=-%`rNTT$Q3+IGsv=@uepU+g& + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/fonts/Roboto-regular/Roboto-regular.ttf b/assets/fonts/Roboto-regular/Roboto-regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7b25f3ce940cbba3001420d38b7d0f12fb7f2142 GIT binary patch literal 32652 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 N6K0jQ!xePm{|7p>-q!#C literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-regular/Roboto-regular.woff b/assets/fonts/Roboto-regular/Roboto-regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..5e353cf47a872d196e9894c8e86aa8a39afc8b5a GIT binary patch literal 13308 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/Roboto-regular/Roboto-regular.woff2 b/assets/fonts/Roboto-regular/Roboto-regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..d1035f9a104819cdd70d3694683c53ce8d9580ae GIT binary patch literal 10292 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 GRf+>&>*#?1 literal 0 HcmV?d00001 diff --git a/assets/fonts/roboto-slimfix.css b/assets/fonts/roboto-slimfix.css new file mode 100644 index 00000000..d2dd8a11 --- /dev/null +++ b/assets/fonts/roboto-slimfix.css @@ -0,0 +1,111 @@ +@font-face { + font-family: 'Roboto'; + font-weight: 400; + font-style: normal; + src: url('/assets/vendor/fonts/Roboto-300/Roboto-300.eot'); + src: url('/assets/vendor/fonts/Roboto-300/Roboto-300.eot?#iefix') format('embedded-opentype'), + local('Roboto Light'), + local('Roboto-300'), + url('/assets/vendor/fonts/Roboto-300/Roboto-300.woff2') format('woff2'), + url('/assets/vendor/fonts/Roboto-300/Roboto-300.woff') format('woff'), + url('/assets/vendor/fonts/Roboto-300/Roboto-300.ttf') format('truetype'), + url('/assets/vendor/fonts/Roboto-300/Roboto-300.svg#Roboto') format('svg'); +} + +@font-face { + font-family: 'Roboto'; + font-weight: 500; + font-style: normal; + src: url('/assets/vendor/fonts/Roboto-regular/Roboto-regular.eot'); + src: url('/assets/vendor/fonts/Roboto-regular/Roboto-regular.eot?#iefix') format('embedded-opentype'), + local('Roboto'), + local('Roboto-regular'), + url('/assets/vendor/fonts/Roboto-regular/Roboto-regular.woff2') format('woff2'), + url('/assets/vendor/fonts/Roboto-regular/Roboto-regular.woff') format('woff'), + url('/assets/vendor/fonts/Roboto-regular/Roboto-regular.ttf') format('truetype'), + url('/assets/vendor/fonts/Roboto-regular/Roboto-regular.svg#Roboto') format('svg'); +} + +@font-face { + font-family: 'Roboto'; + font-weight: 700; + font-style: normal; + src: url('/assets/vendor/fonts/Roboto-500/Roboto-500.eot'); + src: url('/assets/vendor/fonts/Roboto-500/Roboto-500.eot?#iefix') format('embedded-opentype'), + local('Roboto Medium'), + local('Roboto-500'), + url('/assets/vendor/fonts/Roboto-500/Roboto-500.woff2') format('woff2'), + url('/assets/vendor/fonts/Roboto-500/Roboto-500.woff') format('woff'), + url('/assets/vendor/fonts/Roboto-500/Roboto-500.ttf') format('truetype'), + url('/assets/vendor/fonts/Roboto-500/Roboto-500.svg#Roboto') format('svg'); +} + +@font-face { + font-family: 'Roboto'; + font-weight: 900; + font-style: normal; + src: url('/assets/vendor/fonts/Roboto-700/Roboto-700.eot'); + src: url('/assets/vendor/fonts/Roboto-700/Roboto-700.eot?#iefix') format('embedded-opentype'), + local('Roboto Bold'), + local('Roboto-700'), + url('/assets/vendor/fonts/Roboto-700/Roboto-700.woff2') format('woff2'), + url('/assets/vendor/fonts/Roboto-700/Roboto-700.woff') format('woff'), + url('/assets/vendor/fonts/Roboto-700/Roboto-700.ttf') format('truetype'), + url('/assets/vendor/fonts/Roboto-700/Roboto-700.svg#Roboto') format('svg'); +} + +@font-face { + font-family: 'Roboto'; + font-weight: 400; + font-style: italic; + src: url('/assets/vendor/fonts/Roboto-300italic/Roboto-300italic.eot'); + src: url('/assets/vendor/fonts/Roboto-300italic/Roboto-300italic.eot?#iefix') format('embedded-opentype'), + local('Roboto Light Italic'), + local('Roboto-300italic'), + url('/assets/vendor/fonts/Roboto-300italic/Roboto-300italic.woff2') format('woff2'), + url('/assets/vendor/fonts/Roboto-300italic/Roboto-300italic.woff') format('woff'), + url('/assets/vendor/fonts/Roboto-300italic/Roboto-300italic.ttf') format('truetype'), + url('/assets/vendor/fonts/Roboto-300italic/Roboto-300italic.svg#Roboto') format('svg'); +} + +@font-face { + font-family: 'Roboto'; + font-weight: 500; + font-style: italic; + src: url('/assets/vendor/fonts/Roboto-italic/Roboto-italic.eot'); + src: url('/assets/vendor/fonts/Roboto-italic/Roboto-italic.eot?#iefix') format('embedded-opentype'), + local('Roboto Italic'), + local('Roboto-italic'), + url('/assets/vendor/fonts/Roboto-italic/Roboto-italic.woff2') format('woff2'), + url('/assets/vendor/fonts/Roboto-italic/Roboto-italic.woff') format('woff'), + url('/assets/vendor/fonts/Roboto-italic/Roboto-italic.ttf') format('truetype'), + url('/assets/vendor/fonts/Roboto-italic/Roboto-italic.svg#Roboto') format('svg'); +} + +@font-face { + font-family: 'Roboto'; + font-weight: 700; + font-style: italic; + src: url('/assets/vendor/fonts/Roboto-500italic/Roboto-500italic.eot'); + src: url('/assets/vendor/fonts/Roboto-500italic/Roboto-500italic.eot?#iefix') format('embedded-opentype'), + local('Roboto Medium Italic'), + local('Roboto-500italic'), + url('/assets/vendor/fonts/Roboto-500italic/Roboto-500italic.woff2') format('woff2'), + url('/assets/vendor/fonts/Roboto-500italic/Roboto-500italic.woff') format('woff'), + url('/assets/vendor/fonts/Roboto-500italic/Roboto-500italic.ttf') format('truetype'), + url('/assets/vendor/fonts/Roboto-500italic/Roboto-500italic.svg#Roboto') format('svg'); +} + +@font-face { + font-family: 'Roboto'; + font-weight: 900; + font-style: italic; + src: url('/assets/vendor/fonts/Roboto-700italic/Roboto-700italic.eot'); + src: url('/assets/vendor/fonts/Roboto-700italic/Roboto-700italic.eot?#iefix') format('embedded-opentype'), + local('Roboto Bold Italic'), + local('Roboto-700italic'), + url('/assets/vendor/fonts/Roboto-700italic/Roboto-700italic.woff2') format('woff2'), + url('/assets/vendor/fonts/Roboto-700italic/Roboto-700italic.woff') format('woff'), + url('/assets/vendor/fonts/Roboto-700italic/Roboto-700italic.ttf') format('truetype'), + url('/assets/vendor/fonts/Roboto-700italic/Roboto-700italic.svg#Roboto') format('svg'); +} diff --git a/assets/fonts/roboto.css b/assets/fonts/roboto.css new file mode 100644 index 00000000..cae1c904 --- /dev/null +++ b/assets/fonts/roboto.css @@ -0,0 +1,111 @@ +@font-face { + font-family: 'Roboto'; + font-weight: 300; + font-style: normal; + src: url('/assets/vendor/fonts/Roboto-300/Roboto-300.eot'); + src: url('/assets/vendor/fonts/Roboto-300/Roboto-300.eot?#iefix') format('embedded-opentype'), + local('Roboto Light'), + local('Roboto-300'), + url('/assets/vendor/fonts/Roboto-300/Roboto-300.woff2') format('woff2'), + url('/assets/vendor/fonts/Roboto-300/Roboto-300.woff') format('woff'), + url('/assets/vendor/fonts/Roboto-300/Roboto-300.ttf') format('truetype'), + url('/assets/vendor/fonts/Roboto-300/Roboto-300.svg#Roboto') format('svg'); +} + +@font-face { + font-family: 'Roboto'; + font-weight: 400; + font-style: normal; + src: url('/assets/vendor/fonts/Roboto-regular/Roboto-regular.eot'); + src: url('/assets/vendor/fonts/Roboto-regular/Roboto-regular.eot?#iefix') format('embedded-opentype'), + local('Roboto'), + local('Roboto-regular'), + url('/assets/vendor/fonts/Roboto-regular/Roboto-regular.woff2') format('woff2'), + url('/assets/vendor/fonts/Roboto-regular/Roboto-regular.woff') format('woff'), + url('/assets/vendor/fonts/Roboto-regular/Roboto-regular.ttf') format('truetype'), + url('/assets/vendor/fonts/Roboto-regular/Roboto-regular.svg#Roboto') format('svg'); +} + +@font-face { + font-family: 'Roboto'; + font-weight: 500; + font-style: normal; + src: url('/assets/vendor/fonts/Roboto-500/Roboto-500.eot'); + src: url('/assets/vendor/fonts/Roboto-500/Roboto-500.eot?#iefix') format('embedded-opentype'), + local('Roboto Medium'), + local('Roboto-500'), + url('/assets/vendor/fonts/Roboto-500/Roboto-500.woff2') format('woff2'), + url('/assets/vendor/fonts/Roboto-500/Roboto-500.woff') format('woff'), + url('/assets/vendor/fonts/Roboto-500/Roboto-500.ttf') format('truetype'), + url('/assets/vendor/fonts/Roboto-500/Roboto-500.svg#Roboto') format('svg'); +} + +@font-face { + font-family: 'Roboto'; + font-weight: 700; + font-style: normal; + src: url('/assets/vendor/fonts/Roboto-700/Roboto-700.eot'); + src: url('/assets/vendor/fonts/Roboto-700/Roboto-700.eot?#iefix') format('embedded-opentype'), + local('Roboto Bold'), + local('Roboto-700'), + url('/assets/vendor/fonts/Roboto-700/Roboto-700.woff2') format('woff2'), + url('/assets/vendor/fonts/Roboto-700/Roboto-700.woff') format('woff'), + url('/assets/vendor/fonts/Roboto-700/Roboto-700.ttf') format('truetype'), + url('/assets/vendor/fonts/Roboto-700/Roboto-700.svg#Roboto') format('svg'); +} + +@font-face { + font-family: 'Roboto'; + font-weight: 300; + font-style: italic; + src: url('/assets/vendor/fonts/Roboto-300italic/Roboto-300italic.eot'); + src: url('/assets/vendor/fonts/Roboto-300italic/Roboto-300italic.eot?#iefix') format('embedded-opentype'), + local('Roboto Light Italic'), + local('Roboto-300italic'), + url('/assets/vendor/fonts/Roboto-300italic/Roboto-300italic.woff2') format('woff2'), + url('/assets/vendor/fonts/Roboto-300italic/Roboto-300italic.woff') format('woff'), + url('/assets/vendor/fonts/Roboto-300italic/Roboto-300italic.ttf') format('truetype'), + url('/assets/vendor/fonts/Roboto-300italic/Roboto-300italic.svg#Roboto') format('svg'); +} + +@font-face { + font-family: 'Roboto'; + font-weight: 400; + font-style: italic; + src: url('/assets/vendor/fonts/Roboto-italic/Roboto-italic.eot'); + src: url('/assets/vendor/fonts/Roboto-italic/Roboto-italic.eot?#iefix') format('embedded-opentype'), + local('Roboto Italic'), + local('Roboto-italic'), + url('/assets/vendor/fonts/Roboto-italic/Roboto-italic.woff2') format('woff2'), + url('/assets/vendor/fonts/Roboto-italic/Roboto-italic.woff') format('woff'), + url('/assets/vendor/fonts/Roboto-italic/Roboto-italic.ttf') format('truetype'), + url('/assets/vendor/fonts/Roboto-italic/Roboto-italic.svg#Roboto') format('svg'); +} + +@font-face { + font-family: 'Roboto'; + font-weight: 500; + font-style: italic; + src: url('/assets/vendor/fonts/Roboto-500italic/Roboto-500italic.eot'); + src: url('/assets/vendor/fonts/Roboto-500italic/Roboto-500italic.eot?#iefix') format('embedded-opentype'), + local('Roboto Medium Italic'), + local('Roboto-500italic'), + url('/assets/vendor/fonts/Roboto-500italic/Roboto-500italic.woff2') format('woff2'), + url('/assets/vendor/fonts/Roboto-500italic/Roboto-500italic.woff') format('woff'), + url('/assets/vendor/fonts/Roboto-500italic/Roboto-500italic.ttf') format('truetype'), + url('/assets/vendor/fonts/Roboto-500italic/Roboto-500italic.svg#Roboto') format('svg'); +} + +@font-face { + font-family: 'Roboto'; + font-weight: 700; + font-style: italic; + src: url('/assets/vendor/fonts/Roboto-700italic/Roboto-700italic.eot'); + src: url('/assets/vendor/fonts/Roboto-700italic/Roboto-700italic.eot?#iefix') format('embedded-opentype'), + local('Roboto Bold Italic'), + local('Roboto-700italic'), + url('/assets/vendor/fonts/Roboto-700italic/Roboto-700italic.woff2') format('woff2'), + url('/assets/vendor/fonts/Roboto-700italic/Roboto-700italic.woff') format('woff'), + url('/assets/vendor/fonts/Roboto-700italic/Roboto-700italic.ttf') format('truetype'), + url('/assets/vendor/fonts/Roboto-700italic/Roboto-700italic.svg#Roboto') format('svg'); +} diff --git a/assets/icons/README.md b/assets/icons/README.md new file mode 100644 index 00000000..bf72d16b --- /dev/null +++ b/assets/icons/README.md @@ -0,0 +1,3 @@ +# .ICOs + +converted using https://www.icoconverter.com/ diff --git a/assets/icons/pm_dark_128.png b/assets/icons/pm_dark_128.png new file mode 100644 index 0000000000000000000000000000000000000000..99193f24b4398cea26a112b5fb0f90d1bed1b3b5 GIT binary patch literal 10900 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 Sr}+6a-B8y|=b@HU%>MurN5ZuL literal 0 HcmV?d00001 diff --git a/assets/icons/pm_dark_256.png b/assets/icons/pm_dark_256.png new file mode 100644 index 0000000000000000000000000000000000000000..fe9d6acba61b72c38f7c117952969a229f36e76f GIT binary patch literal 16639 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 X<|pD6JaYf*k6Ril+R7glZJz%h$qNJp literal 0 HcmV?d00001 diff --git a/assets/icons/pm_dark_512.ico b/assets/icons/pm_dark_512.ico new file mode 100644 index 0000000000000000000000000000000000000000..fdb10a672de2270395087d9264c2fc137303b9d2 GIT binary patch literal 110291 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 literal 0 HcmV?d00001 diff --git a/assets/icons/pm_dark_512.png b/assets/icons/pm_dark_512.png new file mode 100644 index 0000000000000000000000000000000000000000..05f504cb07ad76409bec94d303ebbb08d2e884d0 GIT binary patch literal 31400 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 literal 0 HcmV?d00001 diff --git a/assets/icons/pm_dark_blue_256.png b/assets/icons/pm_dark_blue_256.png new file mode 100644 index 0000000000000000000000000000000000000000..f15379466db710c397264e1073c45685740a4452 GIT binary patch literal 17006 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�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 literal 0 HcmV?d00001 diff --git a/assets/icons/pm_dark_blue_512.png b/assets/icons/pm_dark_blue_512.png new file mode 100644 index 0000000000000000000000000000000000000000..92a860cc1afe3508a2e89628f338671be5c5932d GIT binary patch literal 26121 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 literal 0 HcmV?d00001 diff --git a/assets/icons/pm_dark_green_256.png b/assets/icons/pm_dark_green_256.png new file mode 100644 index 0000000000000000000000000000000000000000..3c2196974f39ce3f6559bc9c0504c0f47d3b0c3e GIT binary patch literal 18162 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 z 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 literal 0 HcmV?d00001 diff --git a/assets/icons/pm_dark_red_128.png b/assets/icons/pm_dark_red_128.png new file mode 100644 index 0000000000000000000000000000000000000000..7f7449a86eaaeef5a70d06644ffd78ada1e942b4 GIT binary patch literal 11443 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 PKU-+3>B7oYtU~?=3~NHd literal 0 HcmV?d00001 diff --git a/assets/icons/pm_dark_red_256.png b/assets/icons/pm_dark_red_256.png new file mode 100644 index 0000000000000000000000000000000000000000..f77a8f4c22ba05d8f21d317378861055fa94a112 GIT binary patch literal 17932 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 Eg3B?+z5oCK literal 0 HcmV?d00001 diff --git a/assets/icons/pm_dark_red_512.ico b/assets/icons/pm_dark_red_512.ico new file mode 100644 index 0000000000000000000000000000000000000000..e12fbbcc9cb4a609a5c247822c9a48c05e8b6e92 GIT binary patch literal 112150 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