├── .gitignore ├── LICENSE.md ├── README.md ├── cmd └── api-server │ └── main.go ├── config.changelog.md ├── config.default.yaml ├── config.example.yaml ├── go.mod ├── go.sum ├── pkg ├── acme │ ├── account.go │ ├── account_eab.go │ ├── account_key.go │ ├── authorization.go │ ├── certificate.go │ ├── challenge.go │ ├── challenge_resources.go │ ├── directory.go │ ├── encoding.go │ ├── error.go │ ├── http_get.go │ ├── http_post_signed.go │ ├── identifier.go │ ├── jwk.go │ ├── nonces │ │ └── noncemanager.go │ ├── order.go │ ├── order_ari.go │ ├── order_certificate.go │ ├── service.go │ └── signing.go ├── challenges │ ├── alias_config_write.go │ ├── alias_handlers_common.go │ ├── alias_handlers_get.go │ ├── alias_handlers_post.go │ ├── dns_checker │ │ ├── check.go │ │ ├── check_types.go │ │ ├── resolver.go │ │ ├── resolver_pair.go │ │ └── service.go │ ├── identifier_to_domain.go │ ├── providers │ │ ├── config.go │ │ ├── config_write.go │ │ ├── dns01acmedns │ │ │ ├── config.go │ │ │ ├── resources.go │ │ │ └── service.go │ │ ├── dns01acmesh │ │ │ ├── cmd.go │ │ │ ├── resources.go │ │ │ └── service.go │ │ ├── dns01cloudflare │ │ │ ├── cloudflare_types.go │ │ │ ├── configure.go │ │ │ ├── resources.go │ │ │ └── service.go │ │ ├── dns01goacme │ │ │ ├── dns_servers_unix.go │ │ │ ├── dns_servers_windows.go │ │ │ ├── resources.go │ │ │ └── service.go │ │ ├── dns01manual │ │ │ ├── cmd.go │ │ │ ├── resources.go │ │ │ └── service.go │ │ ├── handlers_delete.go │ │ ├── handlers_error.go │ │ ├── handlers_get.go │ │ ├── handlers_post.go │ │ ├── handlers_put.go │ │ ├── http01internal │ │ │ ├── handlers.go │ │ │ ├── resources.go │ │ │ ├── routes.go │ │ │ ├── server.go │ │ │ └── service.go │ │ ├── manager.go │ │ ├── manager_add.go │ │ ├── manager_delete.go │ │ ├── manager_update.go │ │ ├── manager_use.go │ │ ├── manager_validation.go │ │ └── provider.go │ ├── provisioning.go │ ├── service.go │ └── solver.go ├── datatypes │ ├── environment │ │ └── params.go │ ├── job_manager │ │ ├── add.go │ │ ├── do.go │ │ ├── manager.go │ │ └── read.go │ ├── ringbuffer │ │ └── ringbuffer.go │ ├── safecert │ │ ├── ocsp.go │ │ └── safecert.go │ └── safemap │ │ └── safemap.go ├── domain │ ├── acme_accounts │ │ ├── account.go │ │ ├── acme.go │ │ ├── handlers_delete.go │ │ ├── handlers_get.go │ │ ├── handlers_post.go │ │ ├── handlers_post_acme.go │ │ ├── handlers_put.go │ │ ├── handlers_put_acme.go │ │ ├── service.go │ │ └── validation.go │ ├── acme_servers │ │ ├── acme_server.go │ │ ├── acme_service.go │ │ ├── handlers_delete.go │ │ ├── handlers_get.go │ │ ├── handlers_post.go │ │ ├── handlers_put.go │ │ ├── service.go │ │ └── validation.go │ ├── app │ │ ├── app.go │ │ ├── app_create.go │ │ ├── auth │ │ │ ├── handlers_common.go │ │ │ ├── handlers_local.go │ │ │ ├── handlers_oidc.go │ │ │ ├── handlers_status.go │ │ │ ├── local.go │ │ │ ├── oidc.go │ │ │ ├── service.go │ │ │ └── session_manager │ │ │ │ ├── authorization.go │ │ │ │ ├── refresh_session.go │ │ │ │ ├── session_manager.go │ │ │ │ └── validate.go │ │ ├── backup │ │ │ ├── automatic_backup.go │ │ │ ├── fileops_backup.go │ │ │ ├── fileops_delete.go │ │ │ ├── fileops_list.go │ │ │ ├── handlers_delete.go │ │ │ ├── handlers_download.go │ │ │ ├── handlers_list_backup.go │ │ │ ├── handlers_make_backup.go │ │ │ ├── helpers.go │ │ │ └── service.go │ │ ├── configure.go │ │ ├── configure_migrate_v2.go │ │ ├── configure_migrate_v3.go │ │ ├── configure_migrate_v4.go │ │ ├── frontend.go │ │ ├── handlers_control.go │ │ ├── handlers_logs.go │ │ ├── handlers_misc.go │ │ ├── httpclient.go │ │ ├── logger.go │ │ ├── middleware_auth_jwt.go │ │ ├── middleware_common.go │ │ ├── middleware_cors.go │ │ ├── middleware_returnval_handling.go │ │ ├── pprof.go │ │ ├── router.go │ │ ├── router_global_handlers.go │ │ ├── router_make.go │ │ ├── run.go │ │ ├── tls.go │ │ └── updater │ │ │ ├── checker.go │ │ │ ├── handlers.go │ │ │ ├── service.go │ │ │ └── version_info.go │ ├── authorizations │ │ ├── fulfiller.go │ │ └── service.go │ ├── certificates │ │ ├── certificate.go │ │ ├── certificate_extra_extn.go │ │ ├── csr.go │ │ ├── handlers_delete.go │ │ ├── handlers_get.go │ │ ├── handlers_post.go │ │ ├── handlers_put.go │ │ ├── services.go │ │ └── validation.go │ ├── download │ │ ├── errors.go │ │ ├── fetch_key.go │ │ ├── fetch_order.go │ │ ├── header.go │ │ ├── out_certificates.go │ │ ├── out_pfx.go │ │ ├── out_private_cert_chains.go │ │ ├── out_private_certs.go │ │ ├── out_private_keys.go │ │ ├── out_root_chain.go │ │ ├── service.go │ │ └── viaurl.go │ ├── orders │ │ ├── auto_ordering.go │ │ ├── fulfilling.go │ │ ├── fulfilling_add.go │ │ ├── fulfilling_do.go │ │ ├── fulfilling_pem_processing.go │ │ ├── handlers_get.go │ │ ├── handlers_get_download.go │ │ ├── handlers_get_fulfilling.go │ │ ├── handlers_get_post_process.go │ │ ├── handlers_post.go │ │ ├── handlers_post_fulfiller.go │ │ ├── order.go │ │ ├── order_acme_create.go │ │ ├── order_acme_payloads.go │ │ ├── post_process.go │ │ ├── post_process_add.go │ │ ├── post_process_do.go │ │ ├── post_process_do_client.go │ │ ├── post_process_do_script.go │ │ ├── service.go │ │ └── validation.go │ └── private_keys │ │ ├── handlers_delete.go │ │ ├── handlers_get.go │ │ ├── handlers_post.go │ │ ├── handlers_put.go │ │ ├── key.go │ │ ├── key_crypto │ │ ├── algorithm.go │ │ ├── algorithms.go │ │ ├── generate.go │ │ └── pem_decode.go │ │ ├── service.go │ │ └── validation.go ├── output │ ├── errors.go │ ├── file.go │ ├── file_pem.go │ ├── file_pfx.go │ ├── file_zip.go │ ├── json.go │ ├── redaction.go │ └── service.go ├── pagination_sort │ └── pagination_sort.go ├── randomness │ ├── backoff.go │ └── randomness.go ├── storage │ ├── errors.go │ └── sqlite │ │ ├── accounts.go │ │ ├── accounts_delete.go │ │ ├── accounts_get.go │ │ ├── accounts_post.go │ │ ├── accounts_put.go │ │ ├── acme_servers.go │ │ ├── acme_servers_delete.go │ │ ├── acme_servers_get.go │ │ ├── acme_servers_post.go │ │ ├── acme_servers_put.go │ │ ├── backup.go │ │ ├── certificates.go │ │ ├── certificates_delete.go │ │ ├── certificates_get.go │ │ ├── certificates_post.go │ │ ├── certificates_put.go │ │ ├── keys.go │ │ ├── keys_delete.go │ │ ├── keys_get.go │ │ ├── keys_post.go │ │ ├── keys_put.go │ │ ├── nulltypes.go │ │ ├── orders.go │ │ ├── orders_get.go │ │ ├── orders_post.go │ │ ├── orders_put.go │ │ ├── setup.go │ │ ├── setup_migrate_v1.go │ │ ├── setup_migrate_v10.go │ │ ├── setup_migrate_v2.go │ │ ├── setup_migrate_v3.go │ │ ├── setup_migrate_v4.go │ │ ├── setup_migrate_v5.go │ │ ├── setup_migrate_v6.go │ │ ├── setup_migrate_v7.go │ │ ├── setup_migrate_v8.go │ │ ├── setup_migrate_v9.go │ │ ├── time.go │ │ ├── types.go │ │ ├── users.go │ │ ├── users_get.go │ │ └── users_put.go └── validation │ ├── domain.go │ ├── domain_test.go │ ├── email.go │ ├── email_test.go │ ├── id.go │ ├── name.go │ └── name_test.go └── scripts ├── linux ├── _warning_store_your_scripts_in_data_folder ├── acme.sh │ ├── LICENSE.md │ ├── acme.sh │ ├── acme_sh_prep.py │ ├── acme_src.sh │ ├── dnsapi │ │ ├── README.md │ │ ├── dns_1984hosting.sh │ │ ├── dns_acmedns.sh │ │ ├── dns_acmeproxy.sh │ │ ├── dns_active24.sh │ │ ├── dns_ad.sh │ │ ├── dns_ali.sh │ │ ├── dns_alviy.sh │ │ ├── dns_anx.sh │ │ ├── dns_artfiles.sh │ │ ├── dns_arvan.sh │ │ ├── dns_aurora.sh │ │ ├── dns_autodns.sh │ │ ├── dns_aws.sh │ │ ├── dns_azion.sh │ │ ├── dns_azure.sh │ │ ├── dns_beget.sh │ │ ├── dns_bookmyname.sh │ │ ├── dns_bunny.sh │ │ ├── dns_cf.sh │ │ ├── dns_clouddns.sh │ │ ├── dns_cloudns.sh │ │ ├── dns_cn.sh │ │ ├── dns_conoha.sh │ │ ├── dns_constellix.sh │ │ ├── dns_cpanel.sh │ │ ├── dns_curanet.sh │ │ ├── dns_cyon.sh │ │ ├── dns_da.sh │ │ ├── dns_ddnss.sh │ │ ├── dns_desec.sh │ │ ├── dns_df.sh │ │ ├── dns_dgon.sh │ │ ├── dns_dnsexit.sh │ │ ├── dns_dnshome.sh │ │ ├── dns_dnsimple.sh │ │ ├── dns_dnsservices.sh │ │ ├── dns_doapi.sh │ │ ├── dns_domeneshop.sh │ │ ├── dns_dp.sh │ │ ├── dns_dpi.sh │ │ ├── dns_dreamhost.sh │ │ ├── dns_duckdns.sh │ │ ├── dns_durabledns.sh │ │ ├── dns_dyn.sh │ │ ├── dns_dynu.sh │ │ ├── dns_dynv6.sh │ │ ├── dns_easydns.sh │ │ ├── dns_edgecenter.sh │ │ ├── dns_edgedns.sh │ │ ├── dns_euserv.sh │ │ ├── dns_exoscale.sh │ │ ├── dns_fornex.sh │ │ ├── dns_freedns.sh │ │ ├── dns_freemyip.sh │ │ ├── dns_gandi_livedns.sh │ │ ├── dns_gcloud.sh │ │ ├── dns_gcore.sh │ │ ├── dns_gd.sh │ │ ├── dns_geoscaling.sh │ │ ├── dns_googledomains.sh │ │ ├── dns_he.sh │ │ ├── dns_he_ddns.sh │ │ ├── dns_hetzner.sh │ │ ├── dns_hexonet.sh │ │ ├── dns_hostingde.sh │ │ ├── dns_huaweicloud.sh │ │ ├── dns_infoblox.sh │ │ ├── dns_infomaniak.sh │ │ ├── dns_internetbs.sh │ │ ├── dns_inwx.sh │ │ ├── dns_ionos.sh │ │ ├── dns_ionos_cloud.sh │ │ ├── dns_ipv64.sh │ │ ├── dns_ispconfig.sh │ │ ├── dns_jd.sh │ │ ├── dns_joker.sh │ │ ├── dns_kappernet.sh │ │ ├── dns_kas.sh │ │ ├── dns_kinghost.sh │ │ ├── dns_knot.sh │ │ ├── dns_la.sh │ │ ├── dns_leaseweb.sh │ │ ├── dns_lexicon.sh │ │ ├── dns_limacity.sh │ │ ├── dns_linode.sh │ │ ├── dns_linode_v4.sh │ │ ├── dns_loopia.sh │ │ ├── dns_lua.sh │ │ ├── dns_maradns.sh │ │ ├── dns_me.sh │ │ ├── dns_miab.sh │ │ ├── dns_mijnhost.sh │ │ ├── dns_misaka.sh │ │ ├── dns_myapi.sh │ │ ├── dns_mydevil.sh │ │ ├── dns_mydnsjp.sh │ │ ├── dns_mythic_beasts.sh │ │ ├── dns_namecheap.sh │ │ ├── dns_namecom.sh │ │ ├── dns_namesilo.sh │ │ ├── dns_nanelo.sh │ │ ├── dns_nederhost.sh │ │ ├── dns_neodigit.sh │ │ ├── dns_netcup.sh │ │ ├── dns_netlify.sh │ │ ├── dns_nic.sh │ │ ├── dns_njalla.sh │ │ ├── dns_nm.sh │ │ ├── dns_nsd.sh │ │ ├── dns_nsone.sh │ │ ├── dns_nsupdate.sh │ │ ├── dns_nw.sh │ │ ├── dns_oci.sh │ │ ├── dns_omglol.sh │ │ ├── dns_one.sh │ │ ├── dns_online.sh │ │ ├── dns_openprovider.sh │ │ ├── dns_openstack.sh │ │ ├── dns_opnsense.sh │ │ ├── dns_ovh.sh │ │ ├── dns_pdns.sh │ │ ├── dns_pleskxml.sh │ │ ├── dns_pointhq.sh │ │ ├── dns_porkbun.sh │ │ ├── dns_rackcorp.sh │ │ ├── dns_rackspace.sh │ │ ├── dns_rage4.sh │ │ ├── dns_rcode0.sh │ │ ├── dns_regru.sh │ │ ├── dns_scaleway.sh │ │ ├── dns_schlundtech.sh │ │ ├── dns_selectel.sh │ │ ├── dns_selfhost.sh │ │ ├── dns_servercow.sh │ │ ├── dns_simply.sh │ │ ├── dns_technitium.sh │ │ ├── dns_tele3.sh │ │ ├── dns_tencent.sh │ │ ├── dns_timeweb.sh │ │ ├── dns_transip.sh │ │ ├── dns_udr.sh │ │ ├── dns_ultra.sh │ │ ├── dns_unoeuro.sh │ │ ├── dns_variomedia.sh │ │ ├── dns_veesp.sh │ │ ├── dns_vercel.sh │ │ ├── dns_vscale.sh │ │ ├── dns_vultr.sh │ │ ├── dns_websupport.sh │ │ ├── dns_west_cn.sh │ │ ├── dns_world4you.sh │ │ ├── dns_yandex360.sh │ │ ├── dns_yc.sh │ │ ├── dns_zilore.sh │ │ ├── dns_zone.sh │ │ ├── dns_zoneedit.sh │ │ └── dns_zonomi.sh │ ├── dnsapi_cw │ │ ├── dns_1984hosting.sh │ │ ├── dns_acmedns.sh │ │ ├── dns_acmeproxy.sh │ │ ├── dns_active24.sh │ │ ├── dns_ad.sh │ │ ├── dns_ali.sh │ │ ├── dns_alviy.sh │ │ ├── dns_anx.sh │ │ ├── dns_artfiles.sh │ │ ├── dns_arvan.sh │ │ ├── dns_aurora.sh │ │ ├── dns_autodns.sh │ │ ├── dns_aws.sh │ │ ├── dns_azion.sh │ │ ├── dns_azure.sh │ │ ├── dns_beget.sh │ │ ├── dns_bookmyname.sh │ │ ├── dns_bunny.sh │ │ ├── dns_cf.sh │ │ ├── dns_clouddns.sh │ │ ├── dns_cloudns.sh │ │ ├── dns_cn.sh │ │ ├── dns_conoha.sh │ │ ├── dns_constellix.sh │ │ ├── dns_cpanel.sh │ │ ├── dns_curanet.sh │ │ ├── dns_cyon.sh │ │ ├── dns_da.sh │ │ ├── dns_ddnss.sh │ │ ├── dns_desec.sh │ │ ├── dns_df.sh │ │ ├── dns_dgon.sh │ │ ├── dns_dnsexit.sh │ │ ├── dns_dnshome.sh │ │ ├── dns_dnsimple.sh │ │ ├── dns_dnsservices.sh │ │ ├── dns_doapi.sh │ │ ├── dns_domeneshop.sh │ │ ├── dns_dp.sh │ │ ├── dns_dpi.sh │ │ ├── dns_dreamhost.sh │ │ ├── dns_duckdns.sh │ │ ├── dns_durabledns.sh │ │ ├── dns_dyn.sh │ │ ├── dns_dynu.sh │ │ ├── dns_dynv6.sh │ │ ├── dns_easydns.sh │ │ ├── dns_edgecenter.sh │ │ ├── dns_edgedns.sh │ │ ├── dns_euserv.sh │ │ ├── dns_exoscale.sh │ │ ├── dns_fornex.sh │ │ ├── dns_freedns.sh │ │ ├── dns_freemyip.sh │ │ ├── dns_gandi_livedns.sh │ │ ├── dns_gcloud.sh │ │ ├── dns_gcore.sh │ │ ├── dns_gd.sh │ │ ├── dns_geoscaling.sh │ │ ├── dns_googledomains.sh │ │ ├── dns_he.sh │ │ ├── dns_he_ddns.sh │ │ ├── dns_hetzner.sh │ │ ├── dns_hexonet.sh │ │ ├── dns_hostingde.sh │ │ ├── dns_huaweicloud.sh │ │ ├── dns_infoblox.sh │ │ ├── dns_infomaniak.sh │ │ ├── dns_internetbs.sh │ │ ├── dns_inwx.sh │ │ ├── dns_ionos.sh │ │ ├── dns_ionos_cloud.sh │ │ ├── dns_ipv64.sh │ │ ├── dns_ispconfig.sh │ │ ├── dns_jd.sh │ │ ├── dns_joker.sh │ │ ├── dns_kappernet.sh │ │ ├── dns_kas.sh │ │ ├── dns_kinghost.sh │ │ ├── dns_knot.sh │ │ ├── dns_la.sh │ │ ├── dns_leaseweb.sh │ │ ├── dns_lexicon.sh │ │ ├── dns_limacity.sh │ │ ├── dns_linode.sh │ │ ├── dns_linode_v4.sh │ │ ├── dns_loopia.sh │ │ ├── dns_lua.sh │ │ ├── dns_maradns.sh │ │ ├── dns_me.sh │ │ ├── dns_miab.sh │ │ ├── dns_mijnhost.sh │ │ ├── dns_misaka.sh │ │ ├── dns_myapi.sh │ │ ├── dns_mydevil.sh │ │ ├── dns_mydnsjp.sh │ │ ├── dns_mythic_beasts.sh │ │ ├── dns_namecheap.sh │ │ ├── dns_namecom.sh │ │ ├── dns_namesilo.sh │ │ ├── dns_nanelo.sh │ │ ├── dns_nederhost.sh │ │ ├── dns_neodigit.sh │ │ ├── dns_netcup.sh │ │ ├── dns_netlify.sh │ │ ├── dns_nic.sh │ │ ├── dns_njalla.sh │ │ ├── dns_nm.sh │ │ ├── dns_nsd.sh │ │ ├── dns_nsone.sh │ │ ├── dns_nsupdate.sh │ │ ├── dns_nw.sh │ │ ├── dns_oci.sh │ │ ├── dns_omglol.sh │ │ ├── dns_one.sh │ │ ├── dns_online.sh │ │ ├── dns_openprovider.sh │ │ ├── dns_openstack.sh │ │ ├── dns_opnsense.sh │ │ ├── dns_ovh.sh │ │ ├── dns_pdns.sh │ │ ├── dns_pleskxml.sh │ │ ├── dns_pointhq.sh │ │ ├── dns_porkbun.sh │ │ ├── dns_rackcorp.sh │ │ ├── dns_rackspace.sh │ │ ├── dns_rage4.sh │ │ ├── dns_rcode0.sh │ │ ├── dns_regru.sh │ │ ├── dns_scaleway.sh │ │ ├── dns_schlundtech.sh │ │ ├── dns_selectel.sh │ │ ├── dns_selfhost.sh │ │ ├── dns_servercow.sh │ │ ├── dns_simply.sh │ │ ├── dns_technitium.sh │ │ ├── dns_tele3.sh │ │ ├── dns_tencent.sh │ │ ├── dns_timeweb.sh │ │ ├── dns_transip.sh │ │ ├── dns_udr.sh │ │ ├── dns_ultra.sh │ │ ├── dns_unoeuro.sh │ │ ├── dns_variomedia.sh │ │ ├── dns_veesp.sh │ │ ├── dns_vercel.sh │ │ ├── dns_vscale.sh │ │ ├── dns_vultr.sh │ │ ├── dns_websupport.sh │ │ ├── dns_west_cn.sh │ │ ├── dns_world4you.sh │ │ ├── dns_yandex360.sh │ │ ├── dns_yc.sh │ │ ├── dns_zilore.sh │ │ ├── dns_zone.sh │ │ ├── dns_zoneedit.sh │ │ └── dns_zonomi.sh │ └── v3.1.1.txt ├── certwarden.service ├── create-dns.example.sh ├── delete-dns.example.sh ├── install.sh ├── post-processing.example.sh ├── set_permissions.sh └── upgrade.sh └── windows ├── _warning_store_your_scripts_in_data_folder ├── create-dns.example.ps1 ├── delete-dns.example.ps1 └── post-processing.example.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | # instance data storage 2 | /data 3 | 4 | # compiled frontend (this will be added later to releases) 5 | /frontend_build 6 | 7 | # vscode files 8 | /.vscode 9 | 10 | # acme.sh temp folder 11 | /scripts/linux/acme.sh/temp 12 | 13 | # temporary debug files 14 | __debug* 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The license governing this software is located at: 2 | https://github.com/gregtwallace/certwarden/blob/master/LICENSE.md 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cert Warden 2 | (Formerly LeGo CertHub) 3 | 4 | Centralized Certificate Management 5 | Conveniently Leverage Let's Encrypt to Secure Your Infrastructure 6 | 7 | ## More Information 8 | https://www.certwarden.com/ 9 | -------------------------------------------------------------------------------- /cmd/api-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "certwarden-backend/pkg/domain/app" 5 | ) 6 | 7 | func main() { 8 | app.Run() 9 | } 10 | -------------------------------------------------------------------------------- /config.default.yaml: -------------------------------------------------------------------------------- 1 | # This file contains default values the server sets if values are not specified 2 | # in the config file. 3 | 4 | # Initial login credentials: 5 | # username: admin 6 | # password: password 7 | 8 | # NO default config_version 9 | 10 | 'bind_address': '' 11 | 'https_port': 4055 12 | 'http_port': 4050 13 | 14 | 'enable_http_redirect': true 15 | 'log_level': 'info' 16 | 'serve_frontend': true 17 | 'cors_permitted_crossorigins': null 18 | 19 | 'certificate_name': 'serverdefault' 20 | 'disable_hsts': false 21 | 22 | 'enable_pprof': false 23 | 'pprof_http_port': 4065 24 | 'pprof_https_port': 4070 25 | 26 | 'auth': 27 | 'local': 28 | 'enabled': true 29 | 30 | 'updater': 31 | 'auto_check': true 32 | 'channel': 'beta' 33 | 34 | 'backup': 35 | 'enabled': true 36 | 'interval_days': 7 37 | 'retention': 38 | 'max_days': 180 39 | 'max_count': -1 40 | 41 | 'orders': 42 | 'auto_order_enable': true 43 | 'refresh_time_hour': 3 44 | 'refresh_time_minute': 12 45 | 46 | 'challenges': 47 | 'domain_aliases': 48 | 'securedomain.com': 'lesssecuredomain.com' 49 | 'dns_checker': 50 | 'skip_check_wait_seconds': null 51 | 'dns_services': 52 | - 'primary_ip': '1.1.1.1' 53 | 'secondary_ip': '1.0.0.1' 54 | - 'primary_ip': '9.9.9.9' 55 | 'secondary_ip': '149.112.112.112' 56 | - 'primary_ip': '8.8.8.8' 57 | 'secondary_ip': '8.8.4.4' 58 | 'providers': 59 | # If any provider is configured, the default will not be 60 | 'http_01_internal': 61 | - 'domains': 62 | - '*' 63 | 'port': 4060 64 | 'precheck_wait': 0 65 | 'postcheck_wait': 0 66 | -------------------------------------------------------------------------------- /pkg/acme/account_eab.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | // makeExternalAccountBinding creates a signed EAB for a newAccount using the provided params. 4 | func (accountKey *AccountKey) makeExternalAccountBinding(eabKid, eabHmacKey, url string) (*acmeSignedMessage, error) { 5 | eab := new(acmeSignedMessage) 6 | var err error 7 | 8 | // make EAB protected header 9 | // hardcode to newAccount url 10 | eabHeader := protectedHeader{ 11 | Algorithm: "HS256", 12 | KeyId: eabKid, 13 | Url: url, 14 | } 15 | 16 | eab.ProtectedHeader, err = encodeJson(eabHeader) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | // make payload (encoded jwk) 22 | eabPayload, err := accountKey.jwk() 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | eab.Payload, err = encodeJson(eabPayload) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | // sign EAB 33 | err = eab.SignEAB(eabHmacKey) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | // add EAB to the ACME payload 39 | return eab, nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/acme/account_key.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | import ( 4 | "crypto" 5 | "strings" 6 | ) 7 | 8 | // AccountKey is the necessary account / key information for signed message generation 9 | type AccountKey struct { 10 | Key crypto.PrivateKey 11 | Kid string 12 | } 13 | 14 | type KeyAuth string 15 | 16 | // KeyAuthorization uses the AccountKey to create the Key Authorization for a given 17 | // challenge token 18 | func (accountKey *AccountKey) KeyAuthorization(token string) (keyAuth KeyAuth, err error) { 19 | // get jwk 20 | jwk, err := accountKey.jwk() 21 | if err != nil { 22 | return "", err 23 | } 24 | 25 | // calc encoded thumbprint of jwk 26 | encodedThumbprint, err := jwk.encodedSHA256PublicThumbprint() 27 | if err != nil { 28 | return "", err 29 | } 30 | 31 | return KeyAuth(strings.Join([]string{token, encodedThumbprint}, ".")), nil 32 | } 33 | -------------------------------------------------------------------------------- /pkg/acme/authorization.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // ACME authorization response 8 | type Authorization struct { 9 | Identifier Identifier `json:"identifier"` // see orders 10 | Status string `json:"status"` 11 | Expires timeString `json:"expires"` 12 | Challenges []Challenge `json:"challenges"` 13 | Wildcard bool `json:"wildcard,omitempty"` 14 | } 15 | 16 | // Account response decoder 17 | func unmarshalAuthorization(jsonResp json.RawMessage) (auth Authorization, err error) { 18 | err = json.Unmarshal(jsonResp, &auth) 19 | if err != nil { 20 | return Authorization{}, err 21 | } 22 | 23 | return auth, nil 24 | } 25 | 26 | // GetAuth does a POST-as-GET to fetch an authorization object 27 | func (service *Service) GetAuth(authUrl string, accountKey AccountKey) (auth Authorization, err error) { 28 | 29 | // POST-as-GET 30 | jsonResp, _, err := service.PostAsGet(authUrl, accountKey) 31 | if err != nil { 32 | return Authorization{}, err 33 | } 34 | 35 | // unmarshal response 36 | auth, err = unmarshalAuthorization(jsonResp) 37 | if err != nil { 38 | return Authorization{}, err 39 | } 40 | 41 | return auth, nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/acme/certificate.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | import "encoding/pem" 4 | 5 | // revokePayload is the struct to send to ACME to perform a certificate revocation 6 | type revokePayload struct { 7 | Certificate string `json:"certificate"` 8 | Reason int `json:"reason"` 9 | } 10 | 11 | // RevokeCertificate revokes the certificate pem (or pem chain) that is passed in 12 | // using the specfied reason code and account. If ACME revokes the cert, no content 13 | // is returned. Otherwise, if revocation fails, error is returned. 14 | func (service *Service) RevokeCertificate(certPem string, reasonCode int, accountKey AccountKey) (err error) { 15 | // decode pem (if a chain, take the first cert and discard the rest) 16 | pemBlock, _ := pem.Decode([]byte(certPem)) 17 | 18 | // encode the pem bytes for ACME 19 | derCert := encodeString(pemBlock.Bytes) 20 | 21 | // make payload 22 | payload := revokePayload{ 23 | Certificate: derCert, 24 | Reason: reasonCode, 25 | } 26 | 27 | // revoke 28 | _, _, err = service.postToUrlSigned(payload, service.dir.RevokeCert, accountKey) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /pkg/acme/challenge.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // Define challenge types (per RFC 8555) 8 | type ChallengeType string 9 | 10 | const ( 11 | UnknownChallengeType ChallengeType = "" 12 | 13 | ChallengeTypeHttp01 ChallengeType = "http-01" 14 | ChallengeTypeDns01 ChallengeType = "dns-01" 15 | ) 16 | 17 | // ACME challenge object 18 | type Challenge struct { 19 | Type ChallengeType `json:"type"` 20 | Url string `json:"url"` 21 | Status string `json:"status"` 22 | Validated timeString `json:"validated,omitempty"` 23 | Token string `json:"token"` 24 | Error *Error `json:"error,omitempty"` 25 | } 26 | 27 | // Account response decoder 28 | func unmarshalChallenge(jsonResp json.RawMessage) (chall Challenge, err error) { 29 | err = json.Unmarshal(jsonResp, &chall) 30 | if err != nil { 31 | return Challenge{}, err 32 | } 33 | 34 | return chall, nil 35 | } 36 | 37 | // InstructServerToValidateChallenge posts a an empty object to the challenge URL which informs 38 | // ACME that the challenge is ready to be validated 39 | func (service *Service) InstructServerToValidateChallenge(challengeUrl string, accountKey AccountKey) (chall Challenge, err error) { 40 | // post challenge with {} as payload signals the challenge is ready for validation 41 | jsonResp, _, err := service.postToUrlSigned(struct{}{}, challengeUrl, accountKey) 42 | if err != nil { 43 | return Challenge{}, err 44 | } 45 | 46 | // unmarshal response 47 | chall, err = unmarshalChallenge(jsonResp) 48 | if err != nil { 49 | return Challenge{}, err 50 | } 51 | 52 | return chall, nil 53 | } 54 | 55 | // GetChallenge does a POST-as-GET to fetch the current state of the given challenge URL 56 | func (service *Service) GetChallenge(challengeUrl string, key AccountKey) (chall Challenge, err error) { 57 | // POST-as-GET 58 | jsonResp, _, err := service.PostAsGet(challengeUrl, key) 59 | if err != nil { 60 | return Challenge{}, err 61 | } 62 | 63 | // unmarshal response 64 | chall, err = unmarshalChallenge(jsonResp) 65 | if err != nil { 66 | return Challenge{}, err 67 | } 68 | 69 | return chall, nil 70 | } 71 | -------------------------------------------------------------------------------- /pkg/acme/challenge_resources.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | import ( 4 | "crypto/sha256" 5 | ) 6 | 7 | // ValidationResourceDns01 returns the dnsRecord name and value to provision 8 | // in response to a Dns01 challenge for a given domain and keyAuth 9 | func ValidationResourceDns01(domain string, keyAuth KeyAuth) (dnsRecordName string, dnsRecordValue string) { 10 | // dns record name is just the domain prepended with the special acme prefix 11 | dnsRecordName = "_acme-challenge." + domain 12 | 13 | // dns record value is the base64 encoded sha256 of key authorization 14 | // calculate digest 15 | keyAuthDigest := sha256.Sum256([]byte(keyAuth)) 16 | 17 | // encode it 18 | dnsRecordValue = encodeString(keyAuthDigest[:]) 19 | 20 | return dnsRecordName, dnsRecordValue 21 | } 22 | -------------------------------------------------------------------------------- /pkg/acme/encoding.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/binary" 7 | "encoding/json" 8 | "math/big" 9 | "time" 10 | ) 11 | 12 | // encodeString returns an encoded string using the type of encoding 13 | // ACME requires (base64 RawURL format) 14 | func encodeString(data []byte) string { 15 | return base64.RawURLEncoding.EncodeToString(data) 16 | } 17 | 18 | // encodeJson transforms a data object into json and then encodes it 19 | // in the required string format 20 | func encodeJson(data any) (string, error) { 21 | jsonBytes, err := json.Marshal(data) 22 | if err != nil { 23 | return "", err 24 | } 25 | 26 | return encodeString(jsonBytes), nil 27 | } 28 | 29 | // encodeInt returns the value of an int properly encoded for ACME jwk 30 | func encodeInt(integer int) (string, error) { 31 | bytesBuf := new(bytes.Buffer) 32 | 33 | // uint32 also seems to work, but uint does not 34 | err := binary.Write(bytesBuf, binary.BigEndian, uint64(integer)) 35 | if err != nil { 36 | return "", err 37 | } 38 | 39 | // trim off left 00s 40 | // fixes: https://github.com/gregtwallace/certwarden-backend/issues/1 41 | trimmedInt := bytes.TrimLeft(bytesBuf.Bytes(), "\x00") 42 | 43 | return encodeString(trimmedInt), nil 44 | } 45 | 46 | // encodeBigInt returns the bytes of a bigInt properly encoded (based on the 47 | // bit size of the private key) for ACME jwk 48 | func encodeBigInt(bigInt *big.Int, keyBitSize int) (encodedProp string) { 49 | // make buffer based on octet length (essentially divide by 8 and round up) 50 | octetLen := (keyBitSize + 7) >> 3 51 | bytesBuf := make([]byte, octetLen) 52 | 53 | return encodeString(bigInt.FillBytes(bytesBuf)) 54 | } 55 | 56 | // timeString is a string in the date format defined in RFC3339 (which is 57 | // what RFC8555 says to use) 58 | type timeString string 59 | 60 | // ToUnixTime returns the unix time for a timeString. If the timeString is 61 | // nil or fails to parse, 0 is returned. 62 | func (ats *timeString) ToUnixTime() (unixTime *int) { 63 | if ats == nil { 64 | return nil 65 | } 66 | 67 | // RFC3339 68 | layout := "2006-01-02T15:04:05Z" 69 | 70 | // Parse 71 | t, err := time.Parse(layout, string(*ats)) 72 | if err != nil { 73 | return new(int) 74 | } 75 | 76 | ts := int(t.Unix()) 77 | return &ts 78 | } 79 | -------------------------------------------------------------------------------- /pkg/acme/error.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // ACME error 10 | type Error struct { 11 | Status int `json:"status"` 12 | Type string `json:"type"` 13 | Detail string `json:"detail"` 14 | } 15 | 16 | // Error() implements the error interface 17 | func (e *Error) Error() string { 18 | return fmt.Sprintf("status: %d; type: %s; detail: %s", e.Status, e.Type, e.Detail) 19 | } 20 | 21 | // unmarshalErrorResponse attempts to unmarshal into the error response object. If 22 | // it returns nil, the bodyBytes are not an ACME error. 23 | func unmarshalErrorResponse(bodyBytes []byte) *Error { 24 | acmeErr := new(Error) 25 | err := json.Unmarshal(bodyBytes, acmeErr) 26 | // if error decoding was not succesful, not an error 27 | if err != nil { 28 | return nil 29 | } 30 | 31 | // validate the unmarshalled thing is an error, and not just something else that 32 | // unmarshalled without golang error 33 | if !strings.HasPrefix(acmeErr.Type, "urn:ietf:params:acme:error") { 34 | return nil 35 | } 36 | 37 | // if we did get an error response from ACME 38 | return acmeErr 39 | } 40 | 41 | // MarshalledString returns a JSON object as a string. This is useful to 42 | // store an ACME error in storage (e.g. a database string) 43 | func (e *Error) MarshalledString() (*string, error) { 44 | if e == nil { 45 | return nil, nil 46 | } 47 | 48 | errBytes, err := json.Marshal(*e) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | s := new(string) 54 | *s = string(errBytes) 55 | 56 | return s, nil 57 | } 58 | 59 | // NewAcmeError converts a json acme.Error object into an acme.Error 60 | func NewAcmeError(acmeErrorJson *string) (e *Error) { 61 | if acmeErrorJson == nil { 62 | return nil 63 | } 64 | 65 | err := json.Unmarshal([]byte(*acmeErrorJson), e) 66 | if err != nil { 67 | // if unmarshal fails, return nil 68 | return nil 69 | } 70 | 71 | return e 72 | } 73 | -------------------------------------------------------------------------------- /pkg/acme/http_get.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | 10 | "go.uber.org/zap/zapcore" 11 | ) 12 | 13 | // get does an unauthenticated GET request to an ACME endpoint 14 | func (service *Service) get(url string) (bodyBytes []byte, _ http.Header, _ error) { 15 | // do GET 16 | resp, err := service.httpClient.Get(url) 17 | if err != nil { 18 | return nil, nil, fmt.Errorf("acme: get %s failed (%v)", url, err) 19 | } 20 | defer resp.Body.Close() 21 | 22 | // read body of response 23 | bodyBytes, err = io.ReadAll(resp.Body) 24 | if err != nil { 25 | return nil, nil, fmt.Errorf("acme: get %s failed to read body (%v)", url, err) 26 | } 27 | 28 | // ACME response body (debugging) 29 | // indent (if possible) before debug logging 30 | if service.logger.Level() == zapcore.DebugLevel { 31 | var prettyBytes bytes.Buffer 32 | prettyErr := json.Indent(&prettyBytes, bodyBytes, "", "\t") 33 | if prettyErr != nil { 34 | service.logger.Debugf("acme: get %s response code: %d ; body: %s", url, resp.StatusCode, string(bodyBytes)) 35 | } else { 36 | service.logger.Debugf("acme: get %s response code: %d ; body: %s", url, resp.StatusCode, prettyBytes.String()) 37 | } 38 | } 39 | 40 | // try to decode AcmeError 41 | acmeError := unmarshalErrorResponse(bodyBytes) 42 | if acmeError != nil { 43 | return nil, nil, acmeError 44 | } 45 | 46 | // verify status code is success (catch all in case acmeError wasn't present) 47 | if resp.StatusCode < 200 || resp.StatusCode >= 300 { 48 | return bodyBytes, resp.Header, fmt.Errorf("acme: get %s error: status code %d", url, resp.StatusCode) 49 | } 50 | 51 | return bodyBytes, resp.Header, nil 52 | } 53 | 54 | // postAsGet implements POST-as-GET as specified in rfc8555 6.3. 55 | // Specific functions that use this will also need to be defined 56 | func (service *Service) PostAsGet(url string, accountKey AccountKey) (body []byte, headers http.Header, err error) { 57 | return service.postToUrlSigned("", url, accountKey) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/acme/identifier.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | // Identifier is the ACME Identifier object 4 | type Identifier struct { 5 | Type IdentifierType `json:"type"` 6 | Value string `json:"value"` 7 | } 8 | 9 | // Define ACME identifier types (per RFC 8555 9.7.7) 10 | type IdentifierType string 11 | 12 | const ( 13 | UnknownIdentifierType IdentifierType = "" 14 | 15 | IdentifierTypeDns = "dns" 16 | ) 17 | 18 | // IdentifierSlice is a slice of Identifier 19 | type IdentifierSlice []Identifier 20 | 21 | // DnsIdentifiers returns a slice of the value strings for a slice of Identifiers 22 | func (ids *IdentifierSlice) DnsIdentifiers() []string { 23 | var s []string 24 | 25 | for _, id := range *ids { 26 | if id.Type == IdentifierTypeDns { 27 | s = append(s, id.Value) 28 | } 29 | } 30 | 31 | return s 32 | } 33 | -------------------------------------------------------------------------------- /pkg/acme/service.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | import ( 4 | "certwarden-backend/pkg/acme/nonces" 5 | "context" 6 | "errors" 7 | "net/http" 8 | "sync" 9 | 10 | "go.uber.org/zap" 11 | ) 12 | 13 | // App interface is for connecting to the main app 14 | type App interface { 15 | GetLogger() *zap.SugaredLogger 16 | GetHttpClient() *http.Client 17 | GetShutdownContext() context.Context 18 | GetShutdownWaitGroup() *sync.WaitGroup 19 | } 20 | 21 | // Acme service struct 22 | type Service struct { 23 | logger *zap.SugaredLogger 24 | httpClient *http.Client 25 | dirUri string 26 | dir *directory 27 | nonceManager *nonces.Manager 28 | } 29 | 30 | // NewService creates a new service 31 | func NewService(app App, dirUri string) (*Service, error) { 32 | service := new(Service) 33 | 34 | // logger 35 | service.logger = app.GetLogger() 36 | if service.logger == nil { 37 | return nil, errors.New("acme: newservice requires valid logger") 38 | } 39 | 40 | // http client 41 | service.httpClient = app.GetHttpClient() 42 | 43 | // acme directory 44 | service.dirUri = dirUri 45 | service.dir = new(directory) 46 | 47 | // start directory manager 48 | service.backgroundDirManager(app.GetShutdownContext(), app.GetShutdownWaitGroup()) 49 | 50 | // nonce manager 51 | service.nonceManager = nonces.NewManager(service.httpClient, app.GetShutdownContext(), &service.dir.NewNonce) 52 | 53 | return service, nil 54 | } 55 | -------------------------------------------------------------------------------- /pkg/challenges/alias_handlers_common.go: -------------------------------------------------------------------------------- 1 | package challenges 2 | 3 | // output as an array to make use in frontend easier 4 | type domainAliasJson struct { 5 | ChallengeDomain string `json:"challenge_domain"` 6 | ProvisionDomain string `json:"provision_domain"` 7 | } 8 | 9 | // domainAliases returns the current dns aliases as a slice of json 10 | // output objects 11 | func (service *Service) domainAliases() []domainAliasJson { 12 | m := make(map[string]string) 13 | service.dnsIDtoDomain.CopyToMap(m) 14 | 15 | // translate into output array 16 | aliases := []domainAliasJson{} 17 | for k, v := range m { 18 | aliases = append(aliases, domainAliasJson{ 19 | ChallengeDomain: k, 20 | ProvisionDomain: v, 21 | }) 22 | } 23 | 24 | return aliases 25 | } 26 | -------------------------------------------------------------------------------- /pkg/challenges/alias_handlers_get.go: -------------------------------------------------------------------------------- 1 | package challenges 2 | 3 | import ( 4 | "certwarden-backend/pkg/output" 5 | "net/http" 6 | ) 7 | 8 | type domainAliasesResponse struct { 9 | output.JsonResponse 10 | DomainAliases []domainAliasJson `json:"domain_aliases"` 11 | } 12 | 13 | // GetAllProviders returns all of the providers in manager 14 | func (service *Service) GetDomainAliases(w http.ResponseWriter, r *http.Request) *output.JsonError { 15 | // no validation needed 16 | 17 | // write response 18 | response := &domainAliasesResponse{} 19 | response.StatusCode = http.StatusOK 20 | response.Message = "ok" 21 | response.DomainAliases = service.domainAliases() 22 | 23 | err := service.output.WriteJSON(w, response) 24 | if err != nil { 25 | service.logger.Errorf("failed to write json (%s)", err) 26 | return output.JsonErrWriteJsonError(err) 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/challenges/dns_checker/resolver.go: -------------------------------------------------------------------------------- 1 | package dns_checker 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | "time" 8 | ) 9 | 10 | // timeoutSeconds is the DNS dialer timeout (in seconds) 11 | const timeoutSeconds = 5 12 | 13 | var ( 14 | errBlankIP = errors.New("dns_checker: can't create resolver, ip address is blank") 15 | ) 16 | 17 | // makeResolvers generates all of the resolver pairs for a slice 18 | // of DNS Service IP Pairs 19 | func makeResolvers(dnsServices []DnsServiceIPPair) ([]dnsResolverPair, error) { 20 | // add each service pair to the resolver pairs 21 | dnsResolverPairs := []dnsResolverPair{} 22 | for i := range dnsServices { 23 | // make primary 24 | primaryR, err := makeResolver(dnsServices[i].Primary) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | // make secondary (blank is okay, just exclude it) 30 | secondaryR, err := makeResolver(dnsServices[i].Secondary) 31 | if err != nil && !errors.Is(err, errBlankIP) { 32 | return nil, err 33 | } 34 | 35 | // make pair 36 | servicePair := dnsResolverPair{ 37 | primary: primaryR, 38 | secondary: secondaryR, 39 | } 40 | 41 | // append to list of pairs 42 | dnsResolverPairs = append(dnsResolverPairs, servicePair) 43 | } 44 | 45 | return dnsResolverPairs, nil 46 | } 47 | 48 | // makeResolver creates a net.Resolver to resolve DNS queries using 49 | // the specified DNS server IP. 50 | func makeResolver(ipAddress string) (*net.Resolver, error) { 51 | if ipAddress == "" { 52 | return nil, errBlankIP 53 | } 54 | 55 | r := &net.Resolver{ 56 | PreferGo: true, 57 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) { 58 | d := net.Dialer{} 59 | return d.DialContext(ctx, network, ipAddress+":53") 60 | }, 61 | } 62 | 63 | // make sure the dns resolver actually works 64 | ctx, cancel := context.WithTimeout(context.Background(), timeoutSeconds*time.Second) 65 | defer cancel() 66 | 67 | _, err := r.LookupIP(ctx, "ip", "google.com") 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return r, nil 73 | } 74 | -------------------------------------------------------------------------------- /pkg/challenges/dns_checker/resolver_pair.go: -------------------------------------------------------------------------------- 1 | package dns_checker 2 | 3 | import "net" 4 | 5 | // DnsServiceIPPair contains a primary and secondary DNS server for 6 | // a given DNS service 7 | type DnsServiceIPPair struct { 8 | Primary string `yaml:"primary_ip"` 9 | Secondary string `yaml:"secondary_ip"` 10 | } 11 | 12 | // dnsResolverPair contains the net.Resolver pair for a specific DNS service 13 | type dnsResolverPair struct { 14 | primary *net.Resolver 15 | secondary *net.Resolver 16 | } 17 | 18 | // checkDnsRecord attempts to find the specified record using the dnsResolverPair. It 19 | // first tries the primary dns server and if an error is returned it attempts to use 20 | // the secondary server. 21 | func (rPair dnsResolverPair) checkDnsRecord(fqdn string, recordValue string, recordType dnsRecordType) (exists bool, err error) { 22 | // try primary 23 | exists, err = checkDnsRecord(fqdn, recordValue, recordType, rPair.primary) 24 | // if NO error, return exists 25 | if err == nil { 26 | return exists, nil 27 | } 28 | 29 | // if primary errored, try secondary (if there is one) 30 | if rPair.secondary != nil { 31 | exists, err = checkDnsRecord(fqdn, recordValue, recordType, rPair.secondary) 32 | // if NO error, return exists 33 | if err == nil { 34 | return exists, nil 35 | } 36 | } 37 | 38 | // return false/error (neither attempt found the record) 39 | return false, err 40 | } 41 | -------------------------------------------------------------------------------- /pkg/challenges/dns_checker/service.go: -------------------------------------------------------------------------------- 1 | package dns_checker 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "go.uber.org/zap" 9 | ) 10 | 11 | var errServiceComponent = errors.New("dns_checker: necessary service component is missing") 12 | 13 | // App interface is for connecting to the main app 14 | type App interface { 15 | GetShutdownContext() context.Context 16 | GetLogger() *zap.SugaredLogger 17 | } 18 | 19 | // Config is used to configure the service 20 | type Config struct { 21 | SkipCheckWaitSeconds *int `yaml:"skip_check_wait_seconds"` 22 | DnsServices []DnsServiceIPPair `yaml:"dns_services"` 23 | } 24 | 25 | // service struct 26 | type Service struct { 27 | shutdownContext context.Context 28 | logger *zap.SugaredLogger 29 | skipWait time.Duration 30 | dnsResolvers []dnsResolverPair 31 | } 32 | 33 | // NewService creates a new service 34 | func NewService(app App, cfg Config) (service *Service, err error) { 35 | service = new(Service) 36 | 37 | // logger 38 | service.logger = app.GetLogger() 39 | if service.logger == nil { 40 | return nil, errServiceComponent 41 | } 42 | service.logger.Debug("dns_checker: starting service") 43 | 44 | // shutdown context 45 | service.shutdownContext = app.GetShutdownContext() 46 | 47 | // configure resolvers (unless skipping check) 48 | if cfg.SkipCheckWaitSeconds != nil { 49 | service.logger.Warnf("dns_checker: dns record validation disabled, will manually sleep %d seconds instead", *cfg.SkipCheckWaitSeconds) 50 | service.skipWait = time.Duration(*cfg.SkipCheckWaitSeconds) * time.Second 51 | } else { 52 | service.dnsResolvers, err = makeResolvers(cfg.DnsServices) 53 | if err != nil { 54 | // if failed to make resolvers, fallback to sleeping 55 | fallbackSleepSeconds := 120 56 | service.logger.Errorf("dns_checker: failed to configure resolvers (%s), will sleep %d seconds instead of validating dns records", err, fallbackSleepSeconds) 57 | service.skipWait = time.Duration(fallbackSleepSeconds) * time.Second 58 | } else { 59 | // success 60 | service.logger.Debugf("dns_checker: configured dns server pairs: %s", cfg.DnsServices) 61 | } 62 | } 63 | 64 | return service, nil 65 | } 66 | -------------------------------------------------------------------------------- /pkg/challenges/identifier_to_domain.go: -------------------------------------------------------------------------------- 1 | package challenges 2 | 3 | import "strings" 4 | 5 | // dnsIDValuetoDomain returns the fqdn that should be provisioned to satisfy a given ACME 6 | // DNS Identifier Value. Translation from an identifier to a domain is done via the 7 | // dnsIDtoDomain map. 8 | func (service *Service) dnsIDValuetoDomain(dnsIdentifierValue string) string { 9 | // split dns identifier into parts 10 | dnsIDValueSegments := strings.Split(dnsIdentifierValue, ".") 11 | 12 | // check for a match in dnsIDtoDomain map, starting with most specific match 13 | for index := range dnsIDValueSegments { 14 | // assemble identifier to check 15 | identifierValueToCheck := strings.Join(dnsIDValueSegments[index:], ".") 16 | 17 | // look for ID in the map 18 | domain, exists := service.dnsIDtoDomain.Read(identifierValueToCheck) 19 | if exists { 20 | // DONT just return the exact domain; depending on where in the range this is, 21 | // the beginning of the identifier may need to be prepended 22 | prependSubDomain := strings.Join(dnsIDValueSegments[:index], ".") 23 | if prependSubDomain != "" { 24 | prependSubDomain += "." 25 | } 26 | 27 | return prependSubDomain + domain 28 | } 29 | } 30 | 31 | // No match found, return identifierValue without modification 32 | return dnsIdentifierValue 33 | } 34 | -------------------------------------------------------------------------------- /pkg/challenges/providers/dns01acmedns/service.go: -------------------------------------------------------------------------------- 1 | package dns01acmedns 2 | 3 | import ( 4 | "certwarden-backend/pkg/acme" 5 | "errors" 6 | "net/http" 7 | 8 | "go.uber.org/zap" 9 | ) 10 | 11 | var ( 12 | errServiceComponent = errors.New("necessary dns-01 acme-dns component is missing") 13 | ) 14 | 15 | // App interface is for connecting to the main app 16 | type App interface { 17 | GetLogger() *zap.SugaredLogger 18 | GetHttpClient() *http.Client 19 | } 20 | 21 | // provider Service struct 22 | type Service struct { 23 | logger *zap.SugaredLogger 24 | httpClient *http.Client 25 | acmeDnsAddress string 26 | acmeDnsResources []acmeDnsResource 27 | } 28 | 29 | // ChallengeType returns the ACME Challenge Type this provider uses, which is dns-01 30 | func (service *Service) AcmeChallengeType() acme.ChallengeType { 31 | return acme.ChallengeTypeDns01 32 | } 33 | 34 | // Stop is used for any actions needed prior to deleting this provider. If no actions 35 | // are needed, it is just a no-op. 36 | func (service *Service) Stop() error { return nil } 37 | 38 | // NewService creates a new service 39 | func NewService(app App, cfg *Config) (*Service, error) { 40 | // check config 41 | err := validateConfig(cfg) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | service := new(Service) 47 | 48 | // http client 49 | service.httpClient = app.GetHttpClient() 50 | if service.httpClient == nil { 51 | return nil, errServiceComponent 52 | } 53 | 54 | // logger 55 | service.logger = app.GetLogger() 56 | if service.logger == nil { 57 | return nil, errServiceComponent 58 | } 59 | 60 | // acme-dns host address 61 | service.acmeDnsAddress = cfg.HostAddress 62 | 63 | // acme-dns resources that will be updated 64 | service.acmeDnsResources = cfg.Resources 65 | 66 | return service, nil 67 | } 68 | 69 | // Update Service updates the Service to use the new config 70 | func (service *Service) UpdateService(app App, cfg *Config) error { 71 | // if no config, error 72 | if cfg == nil { 73 | return errServiceComponent 74 | } 75 | 76 | // don't need to do anything with "old" Service, just set a new one 77 | newServ, err := NewService(app, cfg) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | // set content of old pointer so anything with the pointer calls the 83 | // updated service 84 | *service = *newServ 85 | 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /pkg/challenges/providers/dns01acmesh/cmd.go: -------------------------------------------------------------------------------- 1 | package dns01acmesh 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | ) 7 | 8 | const ( 9 | acmeShFileName = "acme.sh" 10 | dnsApiCwPath = "/dnsapi_cw" 11 | ) 12 | 13 | // makeCreateCommand creates the command to make a dns record 14 | func (service *Service) makeCreateCommand(dnsRecordName, dnsRecordValue string) *exec.Cmd { 15 | return service.makeCommand(dnsRecordName, dnsRecordValue, false) 16 | } 17 | 18 | // makeDeleteCommand creates the command to delete a dns record 19 | func (service *Service) makeDeleteCommand(dnsRecordName, dnsRecordValue string) *exec.Cmd { 20 | return service.makeCommand(dnsRecordName, dnsRecordValue, true) 21 | } 22 | 23 | // makeCommand makes a command to create or delete a dns record 24 | func (service *Service) makeCommand(dnsRecordName, dnsRecordValue string, delete bool) *exec.Cmd { 25 | // func name 26 | funcName := service.dnsHook + "_add" 27 | if delete { 28 | funcName = service.dnsHook + "_rm" 29 | } 30 | 31 | // make args for command 32 | // `-c` 33 | args := []string{"-c"} 34 | 35 | // actual command `source [path] ; [func] [args]` 36 | args = append(args, "source "+service.acmeShPath+dnsApiCwPath+"/"+service.dnsHook+".sh"+" ; "+funcName+" "+dnsRecordName+" "+dnsRecordValue) 37 | 38 | // make command 39 | cmd := exec.Command(service.shellPath, args...) 40 | 41 | // set command environment 42 | cmd.Env = append(os.Environ(), service.environmentParams.StringSlice()...) 43 | 44 | return cmd 45 | } 46 | -------------------------------------------------------------------------------- /pkg/challenges/providers/dns01acmesh/resources.go: -------------------------------------------------------------------------------- 1 | package dns01acmesh 2 | 3 | import ( 4 | "certwarden-backend/pkg/acme" 5 | "errors" 6 | "os/exec" 7 | ) 8 | 9 | // Provision adds the requested DNS record. 10 | func (service *Service) Provision(domain string, _ string, keyAuth acme.KeyAuth) error { 11 | // get dns record 12 | dnsRecordName, dnsRecordValue := acme.ValidationResourceDns01(domain, keyAuth) 13 | 14 | // run create script 15 | // script command 16 | cmd := service.makeCreateCommand(dnsRecordName, dnsRecordValue) 17 | 18 | // run script command 19 | result, err := cmd.Output() 20 | if err != nil { 21 | // try to get stderr and log it too 22 | exitErr := new(exec.ExitError) 23 | if errors.As(err, &exitErr) { 24 | service.logger.Errorf("acme.sh dns provision script std err: %s", exitErr.Stderr) 25 | } 26 | 27 | service.logger.Errorf("acme.sh dns provision script error: %s", err) 28 | return err 29 | } 30 | service.logger.Debugf("acme.sh dns provision script output: %s", string(result)) 31 | 32 | return nil 33 | } 34 | 35 | // Deprovision deletes the corresponding DNS record. 36 | func (service *Service) Deprovision(domain string, _ string, keyAuth acme.KeyAuth) error { 37 | // get dns record 38 | dnsRecordName, dnsRecordValue := acme.ValidationResourceDns01(domain, keyAuth) 39 | 40 | // script command 41 | cmd := service.makeDeleteCommand(dnsRecordName, dnsRecordValue) 42 | 43 | // run script command 44 | result, err := cmd.Output() 45 | if err != nil { 46 | // try to get stderr and log it too 47 | exitErr := new(exec.ExitError) 48 | if errors.As(err, &exitErr) { 49 | service.logger.Errorf("acme.sh dns deprovision script std err: %s", exitErr.Stderr) 50 | } 51 | 52 | service.logger.Errorf("acme.sh dns deprovision script error: %s", err) 53 | return err 54 | } 55 | service.logger.Debugf("acme.sh dns deprovision script output: %s", string(result)) 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/challenges/providers/dns01goacme/dns_servers_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !js && !windows 2 | 3 | package dns01goacme 4 | 5 | import ( 6 | "net/netip" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // source: https://github.com/qdm12/dns/tree/v2.0.0-beta/pkg/nameserver 12 | 13 | func GetDNSServers() (nameservers []netip.AddrPort) { 14 | const filename = "/etc/resolv.conf" 15 | return getLocalNameservers(filename) 16 | } 17 | 18 | func getLocalNameservers(filename string) (nameservers []netip.AddrPort) { 19 | const defaultNameserverPort = 53 20 | defaultLocalNameservers := []netip.AddrPort{ 21 | netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), defaultNameserverPort), 22 | netip.AddrPortFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 1}), defaultNameserverPort), 23 | } 24 | 25 | data, err := os.ReadFile(filename) 26 | if err != nil { 27 | return defaultLocalNameservers 28 | } 29 | 30 | lines := strings.Split(string(data), "\n") 31 | for _, line := range lines { 32 | if line == "" { 33 | continue 34 | } 35 | fields := strings.Fields(line) 36 | if len(fields) == 0 || fields[0] != "nameserver" { 37 | continue 38 | } 39 | for _, field := range fields[1:] { 40 | ip, err := netip.ParseAddr(field) 41 | if err != nil { 42 | continue 43 | } 44 | nameservers = append(nameservers, 45 | netip.AddrPortFrom(ip, defaultNameserverPort)) 46 | } 47 | } 48 | 49 | if len(nameservers) == 0 { 50 | return defaultLocalNameservers 51 | } 52 | return nameservers 53 | } 54 | -------------------------------------------------------------------------------- /pkg/challenges/providers/dns01goacme/resources.go: -------------------------------------------------------------------------------- 1 | package dns01goacme 2 | 3 | import "certwarden-backend/pkg/acme" 4 | 5 | // Provision adds the corresponding DNS record. It essentially just calls go-acme's 6 | // provider "Present" function 7 | func (service *Service) Provision(domain string, token string, keyAuth acme.KeyAuth) error { 8 | return service.goacmeProvider.Present(domain, token, string(keyAuth)) 9 | } 10 | 11 | // Provision adds the corresponding DNS record. It essentially just calls go-acme's 12 | // provider "Cleanup" function 13 | func (service *Service) Deprovision(domain string, token string, keyAuth acme.KeyAuth) error { 14 | return service.goacmeProvider.CleanUp(domain, token, string(keyAuth)) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/challenges/providers/dns01manual/cmd.go: -------------------------------------------------------------------------------- 1 | package dns01manual 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | ) 7 | 8 | // makeCreateCommand creates the command to make a dns record 9 | func (service *Service) makeCreateCommand(dnsRecordName, dnsRecordValue string) *exec.Cmd { 10 | return service.makeCommand(dnsRecordName, dnsRecordValue, false) 11 | } 12 | 13 | // makeDeleteCommand creates the command to delete a dns record 14 | func (service *Service) makeDeleteCommand(dnsRecordName, dnsRecordValue string) *exec.Cmd { 15 | return service.makeCommand(dnsRecordName, dnsRecordValue, true) 16 | } 17 | 18 | // makeCommand makes a command to create or delete a dns record 19 | func (service *Service) makeCommand(dnsRecordName, dnsRecordValue string, delete bool) *exec.Cmd { 20 | // create or delete? 21 | scriptPath := service.createScriptPath 22 | if delete { 23 | scriptPath = service.deleteScriptPath 24 | } 25 | 26 | // make args for command 27 | // 0 - script name (e.g. /path/to/script.sh) 28 | args := []string{scriptPath} 29 | 30 | // 1 - RecordName (e.g. _acme-challenge.www.example.com) 31 | args = append(args, dnsRecordName) 32 | 33 | // 2 - RecordValue (e.g. XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs) 34 | args = append(args, dnsRecordValue) 35 | 36 | // make command 37 | cmd := exec.Command(service.shellPath, args...) 38 | 39 | // set command environment 40 | cmd.Env = append(os.Environ(), service.environmentParams.StringSlice()...) 41 | 42 | return cmd 43 | } 44 | -------------------------------------------------------------------------------- /pkg/challenges/providers/dns01manual/resources.go: -------------------------------------------------------------------------------- 1 | package dns01manual 2 | 3 | import ( 4 | "certwarden-backend/pkg/acme" 5 | "errors" 6 | "os/exec" 7 | ) 8 | 9 | // Provision adds the corresponding DNS record using the script. 10 | func (service *Service) Provision(domain string, _ string, keyAuth acme.KeyAuth) error { 11 | // get dns record 12 | dnsRecordName, dnsRecordValue := acme.ValidationResourceDns01(domain, keyAuth) 13 | 14 | // run create script 15 | // script command 16 | cmd := service.makeCreateCommand(dnsRecordName, dnsRecordValue) 17 | 18 | // run script command 19 | result, err := cmd.Output() 20 | if err != nil { 21 | // try to get stderr and log it too 22 | exitErr := new(exec.ExitError) 23 | if errors.As(err, &exitErr) { 24 | service.logger.Errorf("acme.sh dns create script std err: %s", exitErr.Stderr) 25 | } 26 | 27 | service.logger.Errorf("dns create script error: %s", err) 28 | return err 29 | } 30 | service.logger.Debugf("dns create script output: %s", string(result)) 31 | 32 | return nil 33 | } 34 | 35 | // Deprovision deletes the corresponding DNS record using the script. 36 | func (service *Service) Deprovision(domain string, _ string, keyAuth acme.KeyAuth) error { 37 | // get dns record 38 | dnsRecordName, dnsRecordValue := acme.ValidationResourceDns01(domain, keyAuth) 39 | 40 | // run delete script 41 | // script command 42 | cmd := service.makeDeleteCommand(dnsRecordName, dnsRecordValue) 43 | 44 | // run script command 45 | result, err := cmd.Output() 46 | if err != nil { 47 | // try to get stderr and log it too 48 | exitErr := new(exec.ExitError) 49 | if errors.As(err, &exitErr) { 50 | service.logger.Errorf("acme.sh dns create script std err: %s", exitErr.Stderr) 51 | } 52 | 53 | service.logger.Errorf("dns delete script error: %s", err) 54 | return err 55 | } 56 | service.logger.Debugf("dns delete script output: %s", string(result)) 57 | 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/challenges/providers/handlers_error.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | var ( 9 | errWrongTag = errors.New("manager provider action failed due to tag mismatch") 10 | 11 | errBadID = func(id int) error { return fmt.Errorf("no provider exists with id %d", id) } 12 | ) 13 | -------------------------------------------------------------------------------- /pkg/challenges/providers/http01internal/handlers.go: -------------------------------------------------------------------------------- 1 | package http01internal 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/julienschmidt/httprouter" 9 | ) 10 | 11 | // challengeHandler responds to the ACME http-01 challenge path. If the requested 12 | // token exists in this service's resources, the keyAuth bytes are sent back to 13 | // the client. If the token is not in the service's resources, a 404 reply is sent. 14 | func (service *Service) challengeHandler(w http.ResponseWriter, r *http.Request) { 15 | // direct no caching, but include some backup options to try and cover all bases to ensure 16 | // the freshest response is always used 17 | w.Header().Set("Cache-Control", "no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate") 18 | w.Header().Set("Pragma", "no-cache") 19 | // set valid but past date (again, to prevent caching) 20 | w.Header().Set("Expires", time.Time{}.Format(http.TimeFormat)) 21 | 22 | // do not allow sniffing 23 | w.Header().Set("X-Content-Type-Options", "nosniff") 24 | 25 | // token from the client request 26 | token := httprouter.ParamsFromContext(r.Context()).ByName("token") 27 | 28 | // try to read resource 29 | keyAuth, exists := service.provisionedResources.Read(token) 30 | 31 | // resource not available, 404 32 | if !exists { 33 | service.logger.Debugf("http-01 challenge resource %s not found", token) 34 | 35 | // write status 404 36 | w.WriteHeader(http.StatusNotFound) 37 | 38 | // done / exit 39 | return 40 | } 41 | 42 | // token was found, write it 43 | service.logger.Debugf("writing resource (name: %s) to http-01 client", token) 44 | 45 | // convert value to content reader for output 46 | contentReader := bytes.NewReader([]byte(keyAuth)) 47 | 48 | // Set Content-Type explicitly 49 | w.Header().Set("Content-Type", "application/octet-stream") 50 | 51 | // ServeContent (filename is not needed here since Content-Type is set explicitly above) 52 | http.ServeContent(w, r, "", time.Time{}, contentReader) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/challenges/providers/http01internal/resources.go: -------------------------------------------------------------------------------- 1 | package http01internal 2 | 3 | import ( 4 | "certwarden-backend/pkg/acme" 5 | "fmt" 6 | ) 7 | 8 | // Provision adds a resource to host 9 | func (service *Service) Provision(_ string, token string, keyAuth acme.KeyAuth) error { 10 | // add new entry 11 | exists, _ := service.provisionedResources.Add(token, keyAuth) 12 | 13 | // if it already exists, log an error and fail (should never happen if challenges is working 14 | // properly) 15 | if exists { 16 | err := fmt.Errorf("http-01 resource name %s already in use, this should never happen", token) 17 | service.logger.Error(err) 18 | return err 19 | } 20 | 21 | return nil 22 | } 23 | 24 | // Deprovision removes a removes a resource from those being hosted 25 | func (service *Service) Deprovision(_ string, token string, _ acme.KeyAuth) error { 26 | // delete entry 27 | delFunc := func(tokenKey string, _ acme.KeyAuth) bool { 28 | return tokenKey == token 29 | } 30 | 31 | deleteOk := service.provisionedResources.DeleteFunc(delFunc) 32 | if !deleteOk { 33 | return fmt.Errorf("http-01 resource %s failed to delete", token) 34 | } 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/challenges/providers/http01internal/routes.go: -------------------------------------------------------------------------------- 1 | package http01internal 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/julienschmidt/httprouter" 7 | ) 8 | 9 | // Routes creates the application's router and adds the routes. 10 | func (service *Service) routes() http.Handler { 11 | router := httprouter.New() 12 | 13 | // acme challenge route, per rfc8555 8.3 14 | router.HandlerFunc(http.MethodGet, "/.well-known/acme-challenge/:token", service.challengeHandler) 15 | 16 | return router 17 | } 18 | -------------------------------------------------------------------------------- /pkg/challenges/providers/manager.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "certwarden-backend/pkg/output" 5 | "context" 6 | "errors" 7 | "net/http" 8 | "sync" 9 | 10 | "go.uber.org/zap" 11 | ) 12 | 13 | // application contains functions manager & child providers will need 14 | type application interface { 15 | GetLogger() *zap.SugaredLogger 16 | GetOutputter() *output.Service 17 | GetConfigFilenameWithPath() string 18 | GetShutdownContext() context.Context 19 | GetHttpClient() *http.Client 20 | GetShutdownWaitGroup() *sync.WaitGroup 21 | } 22 | 23 | // Manager manages the child providers 24 | type Manager struct { 25 | childApp application 26 | logger *zap.SugaredLogger 27 | output *output.Service 28 | configFile string 29 | nextId int 30 | providers []*provider 31 | dP map[string]*provider // domain -> provider 32 | mu sync.RWMutex 33 | } 34 | 35 | func MakeManager(app application, cfg Config) (mgr *Manager, err error) { 36 | // make struct with configs 37 | mgr = &Manager{ 38 | childApp: app, 39 | logger: app.GetLogger(), 40 | output: app.GetOutputter(), 41 | configFile: app.GetConfigFilenameWithPath(), 42 | nextId: 0, 43 | // []*providers 44 | dP: make(map[string]*provider), // domain -> provider 45 | } 46 | 47 | // get all provider cfgs as array 48 | allCfgs := cfg.All() 49 | 50 | // add each provider to manager 51 | for i := range allCfgs { 52 | _, err = mgr.unsafeAddProvider(allCfgs[i].internalCfg, allCfgs[i].providerCfg) 53 | if err != nil { 54 | return nil, err 55 | } 56 | } 57 | 58 | // verify at least one domain / provider exists 59 | if len(mgr.dP) <= 0 { 60 | return nil, errors.New("no challenge providers are properly configured (at least one must be enabled)") 61 | } 62 | 63 | return mgr, nil 64 | } 65 | -------------------------------------------------------------------------------- /pkg/challenges/providers/manager_delete.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | // unsafeDeleteProvider deletes the specified provider from manager 4 | // and deletes its domains. It MUST be called from a Locked thread. 5 | func (mgr *Manager) unsafeDeleteProvider(p *provider) { 6 | // delete each domain that used provider 7 | for _, domain := range p.Domains { 8 | delete(mgr.dP, domain) 9 | } 10 | 11 | // delete provider from provider slice 12 | for i, oneP := range mgr.providers { 13 | // when on correct provider, snip it out 14 | if p == oneP { 15 | mgr.providers = append(mgr.providers[:i], mgr.providers[i+1:]...) 16 | break 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pkg/challenges/providers/manager_update.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | // unsafeUpdateProviderDomains updates the domains serviced by a provider, if no domains 4 | // are specified, no modification is performed 5 | func (mgr *Manager) unsafeUpdateProviderDomains(p *provider, newDomains []string) { 6 | // no domains == no-op 7 | if len(newDomains) <= 0 { 8 | return 9 | } 10 | 11 | // remove existing domain -> p mappings 12 | for _, oldDomain := range p.Domains { 13 | delete(mgr.dP, oldDomain) 14 | } 15 | 16 | // update p's domains 17 | p.Domains = newDomains 18 | 19 | // add each new domain to map 20 | for _, newDomain := range newDomains { 21 | mgr.dP[newDomain] = p 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pkg/challenges/providers/manager_use.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // ProviderFor returns the provider Service for the given acme Identifier. If 9 | // there is no provider for the Identifier, an error is returned instead. 10 | func (mgr *Manager) ProviderFor(fqdn string) (*provider, error) { 11 | mgr.mu.RLock() 12 | defer mgr.mu.RUnlock() 13 | 14 | // if exact domain is in the list, return its provider 15 | p, exists := mgr.dP[fqdn] 16 | if exists { 17 | return p, nil 18 | } 19 | 20 | // find best match from options (if there is a provider for a more specific subdomain, choose that one) 21 | providerDomain := "" 22 | for domain := range mgr.dP { 23 | // include period to avoid matching something like hellodomain.com to domain.com 's provider 24 | if strings.HasSuffix(fqdn, "."+domain) { 25 | // for a provider with the proper suffix, check length of existing match and update 26 | // match if the new match is longer 27 | if len(domain) > len(providerDomain) { 28 | providerDomain = domain 29 | } 30 | } 31 | } 32 | // if a match was found, return its provider 33 | if providerDomain != "" { 34 | return mgr.dP[providerDomain], nil 35 | } 36 | 37 | // if domain was not found, return wild provider if it exists 38 | p, exists = mgr.dP["*"] 39 | if exists { 40 | return p, nil 41 | } 42 | 43 | return nil, fmt.Errorf("could not find a challenge provider for the specified fqdn (%s)", fqdn) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/challenges/providers/manager_validation.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "certwarden-backend/pkg/validation" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | // unsafeValidateDomains verifies that the domains are all valid 10 | // and also that they're available in manager. p is optional and if specified 11 | // domains will also be condidered valid if they're not available but are 12 | // currently assigned to p. If validation succeeds, nil is returned, if it 13 | // fails, an error is returned. 14 | func (mgr *Manager) unsafeValidateDomains(domains []string, p *provider) error { 15 | // verify every domain is properly formatted, or verify this is wildcard cfg (* only) 16 | // and also verify all domains are available in manager 17 | 18 | // if there are none, invalid 19 | if len(domains) <= 0 { 20 | return errors.New("provider doesn't have any domains (must have at least 1)") 21 | } 22 | 23 | // validate domain names 24 | for _, domain := range domains { 25 | // check validity -or- wildcard 26 | if !validation.DomainValid(domain, false) && !(len(domains) == 1 && domains[0] == "*") { 27 | if domain == "*" { 28 | return errors.New("when using wildcard domain * it must be the only specified domain on the provider") 29 | } 30 | return fmt.Errorf("domain %s is not a validly formatted domain", domain) 31 | } 32 | 33 | // check manager availability 34 | currentP, exists := mgr.dP[domain] 35 | if exists && (p == nil || p != currentP) { 36 | return fmt.Errorf("failed to configure domain %s, each domain can only be configured once", domain) 37 | } 38 | } 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/challenges/providers/provider.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "certwarden-backend/pkg/acme" 5 | "time" 6 | ) 7 | 8 | // providerConfig is the interface provider configs must satisfy 9 | type providerConfig interface{} 10 | 11 | // service is an interface for a child provider service 12 | type Service interface { 13 | AcmeChallengeType() acme.ChallengeType 14 | Provision(domain string, token string, keyAuth acme.KeyAuth) (err error) 15 | Deprovision(domain string, token string, keyAuth acme.KeyAuth) (err error) 16 | Stop() error 17 | } 18 | 19 | // provider is the structure of a provider that is being managed 20 | type provider struct { 21 | ID int `json:"id"` 22 | Tag string `json:"tag"` 23 | Type string `json:"type"` 24 | Domains []string `json:"domains"` 25 | PreCheckWaitSeconds int `json:"precheck_wait"` 26 | PostCheckWaitSeconds int `json:"postcheck_wait"` 27 | Config any `json:"config"` 28 | Service `json:"-"` 29 | } 30 | 31 | // WaitDurationPreResourceCheck returns a duration that should be slept after a resource 32 | // is provisioned, but before checks are performed to confirm the existence of the resource. 33 | // This is useful to avoid unncessary early checking if it is known the resoucres take some 34 | // minimum amount of time to propagate. 35 | func (p *provider) WaitDurationPreResourceCheck() time.Duration { 36 | return time.Duration(p.PreCheckWaitSeconds * int(time.Second)) 37 | } 38 | 39 | // WaitDurationPostResourceCheck returns a duration that should be slept after a resource 40 | // is provisioned, and after checks are performed to confirm the existence of the resource 41 | // and those checks confirmed the existence of the resource. 42 | // This is useful to ensure full resource propation, such as cases where the checks may 43 | // have confirmed existence but some additional time is desired to really make sure things 44 | // completely propagated. 45 | func (p *provider) WaitDurationPostResourceCheck() time.Duration { 46 | return time.Duration(p.PostCheckWaitSeconds * int(time.Second)) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/datatypes/job_manager/add.go: -------------------------------------------------------------------------------- 1 | package job_manager 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ErrAddDuplicateJob = errors.New("job manager: cant add job (already exists)") 9 | ErrAddZeroValueJob = errors.New("job manager: cant add job with zero value") 10 | ) 11 | 12 | // AddJob adds the specified job to the manager. It uses the underlying job's 13 | // Equal() function to determine if the job already exists in manager. If the 14 | // job already exists, it is not added again. 15 | func (mgr *Manager[V]) AddJob(job V) error { 16 | // fail if zeroVal 17 | var zeroVal V 18 | if job.Equal(zeroVal) { 19 | return ErrAddZeroValueJob 20 | } 21 | 22 | mgr.Lock() 23 | defer mgr.Unlock() 24 | 25 | // check for equivelant job using job's equal func 26 | workerNumb := mgr.unsafeJobExists(job) 27 | if workerNumb != nil { 28 | return ErrAddDuplicateJob 29 | } 30 | 31 | // add to work queue 32 | mgr.waitingJobs = append(mgr.waitingJobs, job) 33 | 34 | // send ID to the appropriate channel 35 | // async required as this will block until other end of channel reads the job 36 | go func() { 37 | if job.IsHighPriority() { 38 | mgr.highJobsChan <- job 39 | } else { 40 | mgr.lowJobsChan <- job 41 | } 42 | }() 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/datatypes/job_manager/do.go: -------------------------------------------------------------------------------- 1 | package job_manager 2 | 3 | // do updates the job to assign it to a worker and then executes the internal 'real' 4 | // job 5 | func (mgr *Manager[V]) doJob(job V, workerID int) { 6 | // move job from waiting to working 7 | mgr.Lock() 8 | for i, waitingJ := range mgr.waitingJobs { 9 | if waitingJ.Equal(job) { 10 | // remove from waiting 11 | mgr.waitingJobs[i] = mgr.waitingJobs[len(mgr.waitingJobs)-1] 12 | mgr.waitingJobs = mgr.waitingJobs[:len(mgr.waitingJobs)-1] 13 | 14 | // add to worker 15 | mgr.workingJobs[workerID] = job 16 | } 17 | 18 | } 19 | mgr.Unlock() 20 | 21 | // run job 22 | job.Do(workerID) 23 | 24 | // after job completes, remove it from worker 25 | mgr.Lock() 26 | var zeroVal V 27 | mgr.workingJobs[workerID] = zeroVal 28 | mgr.Unlock() 29 | } 30 | -------------------------------------------------------------------------------- /pkg/datatypes/job_manager/read.go: -------------------------------------------------------------------------------- 1 | package job_manager 2 | 3 | // unsafeJobExists searches for an Equal job in manager. If one is found, 4 | // the worker number it is associated with is returned. If the job is in 5 | // queue without a worker, a negative number is returned. If the job is not 6 | // found, nil is returned. 7 | // Manager MUST be AT LEAST RLocked before callign this func. 8 | func (mgr *Manager[V]) unsafeJobExists(job V) *int { 9 | // zero value job will never be in manager 10 | var zeroVal V 11 | if job.Equal(zeroVal) { 12 | return nil 13 | } 14 | 15 | // check workers 16 | for workerID, mgrJ := range mgr.workingJobs { 17 | if !mgrJ.Equal(zeroVal) && job.Equal(mgrJ) { 18 | // copy int so direct access to mgr's int isn't given out 19 | retVal := new(int) 20 | *retVal = workerID 21 | return retVal 22 | } 23 | } 24 | 25 | // check waiting 26 | for i, mgrJ := range mgr.waitingJobs { 27 | if !mgrJ.Equal(zeroVal) && job.Equal(mgrJ) { 28 | i *= -1 29 | return &i 30 | } 31 | } 32 | 33 | return nil 34 | } 35 | 36 | // JobExists searches for an Equal job in manager. If one is found, 37 | // the worker number it is associated with is returned. If the job is in 38 | // queue without a worker, a negative number is returned. If the job is not 39 | // found, nil is returned. 40 | func (mgr *Manager[V]) JobExists(job V) *int { 41 | // zero value job will never be in manager 42 | var zeroVal V 43 | if job.Equal(zeroVal) { 44 | return nil 45 | } 46 | 47 | mgr.RLock() 48 | defer mgr.RUnlock() 49 | 50 | return mgr.unsafeJobExists(job) 51 | } 52 | 53 | // allManagerJobs is a struct to return all of the jobs currently in Manager 54 | type AllManagerJobs[V Job[V]] struct { 55 | WorkingJobs map[int]V // workerID:job 56 | WaitingJobs []V 57 | } 58 | 59 | // AllCurrentJobs returns all of the jobs in manager. Jobs are separated by those 60 | // currently being worked on, and those waiting in the queue. 61 | func (mgr *Manager[V]) AllCurrentJobs() *AllManagerJobs[V] { 62 | mgr.RLock() 63 | defer mgr.RUnlock() 64 | 65 | // working jobs 66 | workingJobs := make(map[int]V) 67 | for i, mgrJob := range mgr.workingJobs { 68 | // copy jobs to new map 69 | workingJobs[i] = mgrJob 70 | } 71 | 72 | // waiting (queue) jobs 73 | waitingJobs := make([]V, len(mgr.waitingJobs)) 74 | _ = copy(waitingJobs, mgr.waitingJobs) 75 | 76 | // return result 77 | return &AllManagerJobs[V]{ 78 | WorkingJobs: workingJobs, 79 | WaitingJobs: waitingJobs, 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pkg/domain/acme_accounts/acme.go: -------------------------------------------------------------------------------- 1 | package acme_accounts 2 | 3 | import "certwarden-backend/pkg/acme" 4 | 5 | // AcmeAccount is the ACME Account object plus some additional 6 | // details for storage. 7 | type AcmeAccount struct { 8 | acme.Account 9 | ID int `json:"-"` 10 | UpdatedAt int `json:"-"` 11 | } 12 | 13 | // emailToContact generates a string slice in the format ACME 14 | // expects (i.e. 'mailto:' is prepended to the email) 15 | func emailToContact(email string) (contact []string) { 16 | if email == "" { 17 | return contact 18 | } 19 | 20 | return append(contact, "mailto:"+email) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/domain/acme_accounts/handlers_delete.go: -------------------------------------------------------------------------------- 1 | package acme_accounts 2 | 3 | import ( 4 | "certwarden-backend/pkg/output" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/julienschmidt/httprouter" 10 | ) 11 | 12 | // DeleteAccount deletes an acme account from storage 13 | func (service *Service) DeleteAccount(w http.ResponseWriter, r *http.Request) *output.JsonError { 14 | // get id from param 15 | idParam := httprouter.ParamsFromContext(r.Context()).ByName("id") 16 | id, err := strconv.Atoi(idParam) 17 | if err != nil { 18 | service.logger.Debug(err) 19 | return output.JsonErrValidationFailed(err) 20 | } 21 | 22 | // validation 23 | // verify account exists 24 | _, outErr := service.getAccount(id) 25 | if outErr != nil { 26 | return outErr 27 | } 28 | 29 | // do not allow delete if there are any certs using the account 30 | if service.storage.AccountHasCerts(id) { 31 | service.logger.Warn("cannot delete account (in use)") 32 | return output.JsonErrDeleteInUse("account") 33 | } 34 | // end validation 35 | 36 | // delete from storage 37 | err = service.storage.DeleteAccount(id) 38 | if err != nil { 39 | service.logger.Error(err) 40 | return output.JsonErrStorageGeneric(err) 41 | } 42 | 43 | // write response 44 | response := &output.JsonResponse{ 45 | StatusCode: http.StatusOK, 46 | Message: fmt.Sprintf("deleted acme account (id: %d)", id), 47 | } 48 | 49 | err = service.output.WriteJSON(w, response) 50 | if err != nil { 51 | service.logger.Errorf("failed to write json (%s)", err) 52 | return output.JsonErrWriteJsonError(err) 53 | } 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/domain/acme_servers/acme_service.go: -------------------------------------------------------------------------------- 1 | package acme_servers 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "sync" 7 | 8 | "go.uber.org/zap" 9 | ) 10 | 11 | // functions so that acme_servers.Service satisfies the App interface 12 | // contained within acme pkg. This allows acme_servers to start up 13 | // new acme.Service 14 | func (serv *Service) GetLogger() *zap.SugaredLogger { 15 | return serv.logger 16 | } 17 | 18 | func (serv *Service) GetHttpClient() *http.Client { 19 | return serv.httpClient 20 | } 21 | 22 | func (serv *Service) GetShutdownContext() context.Context { 23 | return serv.shutdownContext 24 | } 25 | 26 | func (serv *Service) GetShutdownWaitGroup() *sync.WaitGroup { 27 | return serv.shutdownWaitgroup 28 | } 29 | -------------------------------------------------------------------------------- /pkg/domain/acme_servers/handlers_delete.go: -------------------------------------------------------------------------------- 1 | package acme_servers 2 | 3 | import ( 4 | "certwarden-backend/pkg/output" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/julienschmidt/httprouter" 10 | ) 11 | 12 | // DeleteServer deletes an acme server from storage and terminates the 13 | // related service. 14 | func (service *Service) DeleteServer(w http.ResponseWriter, r *http.Request) *output.JsonError { 15 | // get id from param 16 | idParam := httprouter.ParamsFromContext(r.Context()).ByName("id") 17 | id, err := strconv.Atoi(idParam) 18 | if err != nil { 19 | service.logger.Debug(err) 20 | return output.JsonErrValidationFailed(err) 21 | } 22 | 23 | // validation 24 | // verify server exists 25 | _, outErr := service.getServer(id) 26 | if outErr != nil { 27 | return outErr 28 | } 29 | 30 | // do not allow delete if there are any accounts using the server 31 | if service.storage.ServerHasAccounts(id) { 32 | service.logger.Debug("cannot delete server (in use)") 33 | return output.JsonErrDeleteInUse("server") 34 | } 35 | // end validation 36 | 37 | // delete from storage 38 | err = service.storage.DeleteServer(id) 39 | if err != nil { 40 | service.logger.Error(err) 41 | return output.JsonErrStorageGeneric(err) 42 | } 43 | 44 | // delete acme Service 45 | service.mu.Lock() 46 | defer service.mu.Unlock() 47 | delete(service.acmeServers, id) 48 | 49 | // write response 50 | response := &output.JsonResponse{ 51 | StatusCode: http.StatusOK, 52 | Message: fmt.Sprintf("deleted acme server (id: %d)", id), 53 | } 54 | 55 | err = service.output.WriteJSON(w, response) 56 | if err != nil { 57 | service.logger.Errorf("failed to write json (%s)", err) 58 | return output.JsonErrWriteJsonError(err) 59 | } 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/domain/app/auth/handlers_status.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "certwarden-backend/pkg/output" 5 | "net/http" 6 | ) 7 | 8 | // statusResponse is used to provide information about the authentication process so the 9 | // frontend / client can determine appropriate login method(s) 10 | type statusResponse struct { 11 | output.JsonResponse 12 | AuthorizationStatus struct { 13 | Local struct { 14 | Enabled bool `json:"enabled"` 15 | } `json:"local"` 16 | OIDC struct { 17 | Enabled bool `json:"enabled"` 18 | } `json:"oidc"` 19 | } `json:"auth_status"` 20 | } 21 | 22 | // Status returns a response indicating info about Auth status. The route is NOT secure and 23 | // therefore should NOT leak senstive information. 24 | func (service *Service) Status(w http.ResponseWriter, r *http.Request) *output.JsonError { 25 | // return response to client 26 | response := &statusResponse{} 27 | response.StatusCode = http.StatusOK 28 | response.Message = "ok" 29 | // TODO: Actually check what's available 30 | response.AuthorizationStatus.Local.Enabled = service.methodLocalEnabled() 31 | response.AuthorizationStatus.OIDC.Enabled = service.methodOIDCEnabled() 32 | 33 | // write response 34 | err := service.output.WriteJSON(w, response) 35 | if err != nil { 36 | service.logger.Errorf("failed to write json (%s)", err) 37 | // err not detailed as this route will not be secured 38 | return output.JsonErrWriteJsonError(nil) 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/domain/app/auth/local.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | // localExtraFuncs implements session manager's extraFuncs interface 4 | type localExtraFuncs struct { 5 | dbUsername string 6 | storageService Storage 7 | } 8 | 9 | // RefreshCheck for local users just queries the DB to confirm no-error 10 | func (lef *localExtraFuncs) RefreshCheck() error { 11 | // get user must work 12 | _, err := lef.storageService.GetOneUserByName(lef.dbUsername) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/domain/app/auth/session_manager/refresh_session.go: -------------------------------------------------------------------------------- 1 | package session_manager 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | // RefreshSession validates that r contains a valid cookie. If it does, the session in session manager 11 | // is updated with a new authorization and the username and new authorization are returned. If it does 12 | // not, any existing cookie is deleted using w and an error is returned. 13 | func (sm *SessionManager) RefreshSession(r *http.Request, w http.ResponseWriter) (*authorization, error) { 14 | sm.mu.Lock() 15 | defer sm.mu.Unlock() 16 | 17 | // wrap to easily check err and delete cookies 18 | session, err := func() (*session, error) { 19 | // get the session token cookie from request 20 | clientSessionCookie, err := r.Cookie(sessionCookieName) 21 | if err != nil { 22 | return nil, fmt.Errorf("bad cookie: %s", err) 23 | } 24 | 25 | // check for matching session token 26 | var session *session 27 | for _, s := range sm.sessions { 28 | if s.authorization.sessionCookie.Value == clientSessionCookie.Value { 29 | session = s 30 | break 31 | } 32 | } 33 | if session == nil { 34 | return nil, errors.New("invalid cookie value") 35 | } 36 | 37 | // found, check if expired 38 | if time.Now().After(time.Time(session.authorization.SessionExpiration)) { 39 | return nil, errors.New("session expired") 40 | } 41 | 42 | // not expired, return session 43 | return session, nil 44 | }() 45 | 46 | // if err, delete session cookie and return err 47 | if err != nil { 48 | sm.DeleteSessionCookie(w) 49 | return nil, err 50 | } 51 | 52 | // run any extra check 53 | if session.extraFuncs != nil { 54 | err = session.extraFuncs.RefreshCheck() 55 | if err != nil { 56 | sm.DeleteSessionCookie(w) 57 | return nil, err 58 | } 59 | } 60 | 61 | // session was found, update it and return username and new auth 62 | session.authorization, err = sm.newAuthorization(session.authorization.Username, userType(session.authorization.UserType)) 63 | if err != nil { 64 | return nil, fmt.Errorf("couldn't make new auth: %s", err) 65 | } 66 | 67 | return session.authorization, nil 68 | } 69 | -------------------------------------------------------------------------------- /pkg/domain/app/auth/session_manager/validate.go: -------------------------------------------------------------------------------- 1 | package session_manager 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | const authHeader = "Authorization" 10 | 11 | // ValidateAuthHeader validates that the header contains a valid access token. If invalid, 12 | // an error is returned. It also writes to w to indicate the response was impacted by the 13 | // relevant header. 14 | func (sm *SessionManager) ValidateAuthHeader(r *http.Request, w http.ResponseWriter, logTaskName string) (*authorization, error) { 15 | // indicate Authorization header influenced the response 16 | w.Header().Add("Vary", authHeader) 17 | 18 | // if logTaskName unspecified, use a default 19 | if logTaskName == "" { 20 | logTaskName = "validation of auth header" 21 | } 22 | 23 | // get token string from header 24 | clientAccessToken := r.Header.Get(authHeader) 25 | 26 | // anonymous user 27 | if clientAccessToken == "" { 28 | err := fmt.Errorf("client %s: %s failed (access token is missing)", r.RemoteAddr, logTaskName) 29 | sm.logger.Debug(err) 30 | return nil, err 31 | } 32 | 33 | // validate token 34 | sm.mu.RLock() 35 | defer sm.mu.RUnlock() 36 | 37 | var session *session 38 | for _, s := range sm.sessions { 39 | if s.authorization.AccessToken == clientAccessToken { 40 | session = s 41 | break 42 | } 43 | } 44 | if session == nil { 45 | err := fmt.Errorf("client %s: %s failed (invalid access token)", r.RemoteAddr, logTaskName) 46 | sm.logger.Debug(err) 47 | return nil, err 48 | } 49 | 50 | // found, check if expired 51 | if time.Now().After(time.Time(session.authorization.AccessTokenExpiration)) { 52 | err := fmt.Errorf("client %s: %s failed (access token expired)", r.RemoteAddr, logTaskName) 53 | sm.logger.Debug(err) 54 | return nil, err 55 | } 56 | 57 | return session.authorization, nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/domain/app/backup/fileops_list.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // backupFileDetails contains information about an on disk backup file 8 | type backupFileDetails struct { 9 | Name string `json:"name"` 10 | Size int `json:"size"` 11 | ModTime int `json:"modtime"` 12 | CreatedAt *int `json:"created_at,omitempty"` 13 | } 14 | 15 | // time returns created_at if it exists, otherwise it returns modtime 16 | func (backupFile *backupFileDetails) unixTime() int { 17 | if backupFile.CreatedAt != nil { 18 | return *backupFile.CreatedAt 19 | } 20 | 21 | // else use modtime 22 | return backupFile.ModTime 23 | } 24 | 25 | // listBackupFiles returns a list of the backup files on the server 26 | func (service *Service) listBackupFiles() ([]backupFileDetails, error) { 27 | // read file list from backup dir 28 | files, err := os.ReadDir(service.cleanDataStorageBackupPath) 29 | if err != nil { 30 | service.logger.Errorf("failed to list backup directory contents (%s)", err) 31 | return nil, err 32 | } 33 | 34 | // for each file, add it to json response list if it is a backup file 35 | backupFilesInfo := []backupFileDetails{} 36 | for i := range files { 37 | // ignore directories 38 | if files[i].IsDir() { 39 | continue 40 | } 41 | 42 | // get name 43 | bakFile := backupFileDetails{} 44 | bakFile.Name = files[i].Name() 45 | 46 | // only list if it is a backup file 47 | if !isBackupFileName(bakFile.Name) { 48 | continue 49 | } 50 | 51 | // stat file 52 | fStat, err := files[i].Info() 53 | if err != nil { 54 | // if cant stat, bad file, skip it 55 | service.logger.Warnf("backup file %s in backup dir that cant be stat'd (%s)", bakFile.Name, err) 56 | continue 57 | } 58 | 59 | // populate file properties 60 | bakFile.Size = int(fStat.Size()) 61 | bakFile.ModTime = int(fStat.ModTime().Unix()) 62 | 63 | // calculate created at from filename, omit if doesn't decode 64 | nameTime, err := backupZipTime(bakFile.Name) 65 | if err != nil { 66 | service.logger.Warnf("backup file with improperly formatted timestamp in backup dir (err decoding time: %s)", err) 67 | } else { 68 | bakFile.CreatedAt = new(int) 69 | *bakFile.CreatedAt = int(nameTime.Unix()) 70 | } 71 | 72 | // add to list 73 | backupFilesInfo = append(backupFilesInfo, bakFile) 74 | } 75 | 76 | return backupFilesInfo, nil 77 | } 78 | -------------------------------------------------------------------------------- /pkg/domain/app/backup/handlers_delete.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "certwarden-backend/pkg/output" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/julienschmidt/httprouter" 12 | ) 13 | 14 | // DeleteDiskBackupHandler deletes an existing backup from the server's disk. 15 | func (service *Service) DeleteDiskBackupHandler(w http.ResponseWriter, r *http.Request) *output.JsonError { 16 | // params 17 | filenameParam := httprouter.ParamsFromContext(r.Context()).ByName("filename") 18 | 19 | // validate filename is in the form of a backup file (prevent unauthorized file download) 20 | if !isBackupFileName(filenameParam) { 21 | return output.JsonErrValidationFailed(errors.New("invalid filename")) 22 | } 23 | 24 | // stat file to confirm it exists 25 | _, err := os.Stat(service.cleanDataStorageBackupPath + string(filepath.Separator) + filenameParam) 26 | if err != nil { 27 | // 404 for file doesn't exist 28 | if errors.Is(err, os.ErrNotExist) { 29 | return output.JsonErrNotFound(errors.New(service.cleanDataStorageBackupPath + string(filepath.Separator) + filenameParam)) 30 | } 31 | // internal for any other issue 32 | err = fmt.Errorf("failed to stat disk backup for delete (%s)", err) 33 | service.logger.Error(err) 34 | return output.JsonErrInternal(err) 35 | } 36 | 37 | // delete file 38 | err = os.Remove(service.cleanDataStorageBackupPath + string(filepath.Separator) + filenameParam) 39 | if err != nil { 40 | err = fmt.Errorf("failed to delete disk backup (%s)", err) 41 | service.logger.Error(err) 42 | return output.JsonErrInternal(err) 43 | } 44 | 45 | service.logger.Infof("backup deleted from disk (%s)", filenameParam) 46 | 47 | // write response 48 | response := &output.JsonResponse{ 49 | StatusCode: http.StatusOK, 50 | Message: fmt.Sprintf("deleted disk backup (%s)", filenameParam), 51 | } 52 | 53 | err = service.output.WriteJSON(w, response) 54 | if err != nil { 55 | service.logger.Errorf("failed to write json (%s)", err) 56 | return output.JsonErrWriteJsonError(err) 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /pkg/domain/app/backup/handlers_list_backup.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "certwarden-backend/pkg/output" 5 | "net/http" 6 | ) 7 | 8 | type backupFileListResponse struct { 9 | output.JsonResponse 10 | Config *Config `json:"config"` 11 | BackupFiles []backupFileDetails `json:"backup_files"` 12 | } 13 | 14 | // ListDiskBackupsHandler returns a list of the backups currently on the disk 15 | // as well as some information about them 16 | func (service *Service) ListDiskBackupsHandler(w http.ResponseWriter, r *http.Request) *output.JsonError { 17 | // get file list 18 | filesInfo, err := service.listBackupFiles() 19 | if err != nil { 20 | return output.JsonErrInternal(err) 21 | } 22 | 23 | // write response 24 | response := &backupFileListResponse{} 25 | response.StatusCode = http.StatusOK 26 | response.Message = "ok" 27 | response.Config = service.config 28 | response.BackupFiles = filesInfo 29 | 30 | err = service.output.WriteJSON(w, response) 31 | if err != nil { 32 | service.logger.Errorf("failed to write json (%s)", err) 33 | return output.JsonErrWriteJsonError(err) 34 | } 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/domain/app/backup/handlers_make_backup.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "certwarden-backend/pkg/output" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | type backupFileMakeResponse struct { 10 | output.JsonResponse 11 | BackupFile backupFileDetails `json:"backup_file"` 12 | } 13 | 14 | // makeDiskBackupNowHandler creates a new backup of the application in the backup 15 | // folder location; it does not send the backup to the client 16 | func (service *Service) MakeDiskBackupNowHandler(w http.ResponseWriter, r *http.Request) *output.JsonError { 17 | backupFileDetails, err := service.CreateBackupOnDisk() 18 | if err != nil { 19 | err = fmt.Errorf("failed to make on disk backup (%s)", err) 20 | service.logger.Error(err) 21 | return output.JsonErrInternal(err) 22 | } 23 | 24 | // write success response 25 | response := &backupFileMakeResponse{} 26 | response.StatusCode = http.StatusCreated 27 | response.Message = fmt.Sprintf("%s written to server storage", backupFileDetails.Name) 28 | response.BackupFile = backupFileDetails 29 | 30 | err = service.output.WriteJSON(w, response) 31 | if err != nil { 32 | service.logger.Errorf("failed to write json (%s)", err) 33 | return output.JsonErrWriteJsonError(err) 34 | } 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/domain/app/backup/helpers.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | ) 7 | 8 | const backupFilePrefix = "cert_warden_backup." 9 | const backupFileSuffix = ".zip" 10 | 11 | // makeBackupZipFileName creates the filename for a new backup created now 12 | func makeBackupZipFileName() (filename string, createdAt int) { 13 | createdTime := time.Now() 14 | 15 | name := backupFilePrefix + createdTime.Local().Format(time.RFC3339) + backupFileSuffix 16 | return strings.ReplaceAll(name, ":", "--"), int(createdTime.Unix()) 17 | } 18 | 19 | // getBackupZipFileTime attempts to return the time from the backup zip filename 20 | func backupZipTime(name string) (time.Time, error) { 21 | name = strings.ReplaceAll(name, "--", ":") 22 | timeString := strings.TrimSuffix(strings.TrimPrefix(name, backupFilePrefix), backupFileSuffix) 23 | 24 | fileTime, err := time.Parse(time.RFC3339, timeString) 25 | if err != nil { 26 | return time.Time{}, err 27 | } 28 | 29 | return fileTime, nil 30 | } 31 | 32 | // isBackupFileName returns true if the fileName string provided starts with the 33 | // backup file prefix and ends in the proper file extension; it also only permits 34 | // certain characters in the filename to avoid things like path traversal 35 | func isBackupFileName(fileName string) bool { 36 | return strings.HasPrefix(fileName, backupFilePrefix) && strings.HasSuffix(fileName, backupFileSuffix) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/domain/app/backup/service.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "certwarden-backend/pkg/output" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "sync" 11 | 12 | "go.uber.org/zap" 13 | ) 14 | 15 | var errServiceComponent = errors.New("necessary backup service component is missing") 16 | 17 | // App interface is for connecting to the main app 18 | type App interface { 19 | GetDataStorageRootPath() string 20 | GetLogger() *zap.SugaredLogger 21 | GetOutputter() *output.Service 22 | LockSQLForBackup() (unlockFunc func(), err error) 23 | GetShutdownContext() context.Context 24 | GetShutdownWaitGroup() *sync.WaitGroup 25 | } 26 | 27 | // Keys service struct 28 | type Service struct { 29 | cleanDataStorageRootPath string 30 | cleanDataStorageBackupPath string 31 | lockSQLForBackup func() (unlockFunc func(), err error) 32 | logger *zap.SugaredLogger 33 | output *output.Service 34 | config *Config 35 | } 36 | 37 | // NewService creates a new service 38 | func NewService(app App) (*Service, error) { 39 | service := new(Service) 40 | 41 | service.cleanDataStorageRootPath = filepath.Clean(app.GetDataStorageRootPath()) 42 | service.cleanDataStorageBackupPath = filepath.Clean(app.GetDataStorageRootPath() + "/" + dataStorageBackupDirName) 43 | 44 | // logger 45 | service.logger = app.GetLogger() 46 | if service.logger == nil { 47 | return nil, errServiceComponent 48 | } 49 | 50 | // output service 51 | service.output = app.GetOutputter() 52 | if service.output == nil { 53 | return nil, errServiceComponent 54 | } 55 | 56 | // create backup storage folder, if doesn't exist 57 | err := os.MkdirAll(service.cleanDataStorageBackupPath, 0755) 58 | if err != nil { 59 | return nil, fmt.Errorf("backup: failed to make directory for on disk backups (%s)", err) 60 | } 61 | 62 | // storage lock func 63 | service.lockSQLForBackup = app.LockSQLForBackup 64 | 65 | // do not start auto service 66 | // must be started later in app (after config is read) 67 | service.config = &Config{} 68 | service.config.Enabled = new(bool) 69 | *service.config.Enabled = false 70 | 71 | return service, nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/domain/app/configure_migrate_v2.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "fmt" 4 | 5 | // CHANGES v1 to v2: 6 | // - cors_permitted_origins renamed to cors_permitted_crossorigins 7 | 8 | // configMigrateV1toV2 modifies the unmarhsalled yaml of the config file 9 | // to migrate the config from version 1 to version 2. an error is returned 10 | // if the migration cannot be performed. 11 | func configMigrateV1toV2(cfgFileYamlObj map[string]any) (newCfgVer int, err error) { 12 | currentSchemaVersion := 1 13 | newSchemaVersion := 2 14 | 15 | if cfgFileYamlObj["config_version"] != currentSchemaVersion { 16 | return -1, fmt.Errorf("cannot update schema, current version %d (expected %d)", cfgFileYamlObj["config_version"], currentSchemaVersion) 17 | } 18 | 19 | // set config version 20 | cfgFileYamlObj["config_version"] = newSchemaVersion 21 | 22 | // if old has cors_permitted_origins, set new cors_permitted_crossorigins from old & delete old 23 | corsPermittedOrigins, ok := cfgFileYamlObj["cors_permitted_origins"] 24 | if ok { 25 | cfgFileYamlObj["cors_permitted_crossorigins"] = corsPermittedOrigins 26 | delete(cfgFileYamlObj, "cors_permitted_origins") 27 | } 28 | 29 | return newSchemaVersion, nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/domain/app/configure_migrate_v3.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "fmt" 4 | 5 | // CHANGES v2 to v3: 6 | // - pprof_port renamed to pprof_http_port 7 | // - pprof_https_port added 8 | 9 | // configMigrateV2toV3 modifies the unmarhsalled yaml of the config file 10 | // to migrate the config from version 2 to version 3. an error is returned 11 | // if the migration cannot be performed. 12 | func configMigrateV2toV3(cfgFileYamlObj map[string]any) (newCfgVer int, err error) { 13 | currentSchemaVersion := 2 14 | newSchemaVersion := 3 15 | 16 | if cfgFileYamlObj["config_version"] != currentSchemaVersion { 17 | return -1, fmt.Errorf("cannot update schema, current version %d (expected %d)", cfgFileYamlObj["config_version"], currentSchemaVersion) 18 | } 19 | 20 | // set config version 21 | cfgFileYamlObj["config_version"] = newSchemaVersion 22 | 23 | // if old has pprof_port, set new pprof_http_port from old & delete old 24 | pprofPort, ok := cfgFileYamlObj["pprof_port"] 25 | if ok { 26 | cfgFileYamlObj["pprof_http_port"] = pprofPort 27 | delete(cfgFileYamlObj, "pprof_port") 28 | } 29 | 30 | return newSchemaVersion, nil 31 | } 32 | -------------------------------------------------------------------------------- /pkg/domain/app/handlers_control.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "certwarden-backend/pkg/output" 5 | "net/http" 6 | ) 7 | 8 | // doShutdownHandler shuts the app down completely. 9 | // Note: It may still restart if the caller is configured to restart it 10 | // if it stops (e.g. when running as a service). 11 | func (app *Application) doShutdownHandler(w http.ResponseWriter, r *http.Request) *output.JsonError { 12 | // write response first since the action will shutdown server 13 | response := &output.JsonResponse{} 14 | response.StatusCode = http.StatusOK 15 | response.Message = "shutting down" 16 | 17 | err := app.output.WriteJSON(w, response) 18 | if err != nil { 19 | app.logger.Errorf("failed to write json (%s)", err) 20 | return output.JsonErrWriteJsonError(err) 21 | } 22 | 23 | // log shutdown 24 | app.logger.Infof("client %s: triggered graceful shutdown via api", r.RemoteAddr) 25 | 26 | // do shutdown 27 | app.shutdown(false) 28 | 29 | return nil 30 | } 31 | 32 | // doRestartHandler shuts the app down and then calls the OS to execute the app 33 | // again with the same args and environment as originally used. 34 | func (app *Application) doRestartHandler(w http.ResponseWriter, r *http.Request) *output.JsonError { 35 | // write response first since the action will shutdown server 36 | response := &output.JsonResponse{} 37 | response.StatusCode = http.StatusOK 38 | response.Message = "restarting" 39 | 40 | err := app.output.WriteJSON(w, response) 41 | if err != nil { 42 | app.logger.Errorf("failed to write json (%s)", err) 43 | return output.JsonErrWriteJsonError(err) 44 | } 45 | 46 | // log restart 47 | app.logger.Infof("client %s: triggered graceful restart via api", r.RemoteAddr) 48 | 49 | // do shutdown 50 | app.shutdown(true) 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /pkg/domain/app/handlers_misc.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "certwarden-backend/pkg/output" 5 | "certwarden-backend/pkg/storage/sqlite" 6 | "net/http" 7 | ) 8 | 9 | // serverStatusResponse 10 | type serverStatusResponse struct { 11 | output.JsonResponse 12 | ServerStatus struct { 13 | Status string `json:"status"` 14 | LogLevel string `json:"log_level"` 15 | Version string `json:"version"` 16 | ConfigVersion int `json:"config_version"` 17 | DbUserVersion int `json:"database_version"` 18 | } `json:"server"` 19 | } 20 | 21 | // statusHandler writes some basic info about the status of the Application 22 | func (app *Application) statusHandler(w http.ResponseWriter, r *http.Request) *output.JsonError { 23 | // write response 24 | response := &serverStatusResponse{} 25 | response.StatusCode = http.StatusOK 26 | response.Message = "ok" 27 | response.ServerStatus.Status = "available" 28 | response.ServerStatus.LogLevel = app.logger.Level().String() 29 | response.ServerStatus.Version = appVersion 30 | response.ServerStatus.ConfigVersion = *app.config.ConfigVersion 31 | response.ServerStatus.DbUserVersion = sqlite.DbCurrentUserVersion 32 | 33 | err := app.output.WriteJSON(w, response) 34 | if err != nil { 35 | app.logger.Errorf("failed to write json (%s)", err) 36 | return output.JsonErrWriteJsonError(err) 37 | } 38 | 39 | return nil 40 | } 41 | 42 | // healthHandler writes some basic info about the status of the Application 43 | func healthHandler(w http.ResponseWriter, r *http.Request) *output.JsonError { 44 | // write 204 (No Content) 45 | w.WriteHeader(http.StatusNoContent) 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /pkg/domain/app/middleware_auth_jwt.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "certwarden-backend/pkg/domain/app/auth" 5 | "certwarden-backend/pkg/output" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | // middlewareApplyAuthJWT applies middleware that validates the jwt access token 11 | // contained in the auth header. If it is not valid, an error is returned instead of 12 | // executing next. 13 | func middlewareApplyAuthJWT(next handlerFunc, auth *auth.Service) handlerFunc { 14 | return func(w http.ResponseWriter, r *http.Request) *output.JsonError { 15 | // shorten URI for logging 16 | trimmedURI := loggableRequestURI(r) 17 | 18 | err := auth.ValidateAuthHeader(r, w, fmt.Sprintf("%s %s", r.Method, trimmedURI)) 19 | if err != nil { 20 | // Note: Do NOT send detailed error since unauthorized 21 | return output.JsonErrUnauthorized 22 | } 23 | 24 | // if valid, do next 25 | return next(w, r) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/domain/app/middleware_common.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // middlewareApplyHSTS applies the HTTP Strict Transport Security (HSTS) header. 8 | func middlewareApplyHSTS(next http.Handler) http.Handler { 9 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 10 | // set HSTS header 11 | w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains") 12 | 13 | // serve next 14 | next.ServeHTTP(w, r) 15 | }) 16 | } 17 | 18 | // middlewareApplyBrowserSecurityHeaders applies a number of security headers to 19 | // reduce the danger of things like click jacking or loading malicious data. these 20 | // are under common as additional precaution. even though many routes are not 21 | // intended for use in a browser, these don't hurt. 22 | func middlewareApplyBrowserSecurityHeaders(next http.Handler) http.Handler { 23 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 24 | // 1) header: Content-Security-Policy - disable loading of other content 25 | // Note: this can be overwritten by the frontend handler, when needed 26 | var contentSecurityPolicy = []string{ 27 | "default-src 'none'", 28 | "base-uri 'none'", 29 | "form-action 'none'", 30 | "frame-ancestors 'none'", 31 | } 32 | 33 | csp := "" 34 | for _, s := range contentSecurityPolicy { 35 | csp += s + "; " 36 | } 37 | 38 | w.Header().Set("Content-Security-Policy", csp) 39 | 40 | // 2) header: X-Content-Type-Options - no MIME type sniffing 41 | w.Header().Set("X-Content-Type-Options", "nosniff") 42 | 43 | // 3) header: X-Frame-Options - do NOT allow frames 44 | w.Header().Set("X-Frame-Options", "deny") 45 | 46 | // 4) header: Referrer - tell browser to never send Referer header 47 | w.Header().Set("Referrer-Policy", "no-referrer") 48 | 49 | // serve next 50 | next.ServeHTTP(w, r) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/domain/app/middleware_cors.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "certwarden-backend/pkg/output" 5 | "net/http" 6 | 7 | "github.com/rs/cors" 8 | ) 9 | 10 | // middlewareApplyCORS applies the CORS package which manages all CORS headers. 11 | // if no cross origins are permitted, this function is a no-op and just returns next 12 | func middlewareApplyCORS(next handlerFunc, permittedCrossOrigins []string) handlerFunc { 13 | // are any cross origins allowed? if not, do not use CORS 14 | if permittedCrossOrigins == nil { 15 | return next 16 | } 17 | 18 | // set up CORS 19 | c := cors.New(cors.Options{ 20 | // permitted cross origins 21 | // WARNING: nil / empty slice == allow all! 22 | AllowedOrigins: permittedCrossOrigins, 23 | 24 | // credentials must be allowed for access to work properly 25 | AllowCredentials: true, 26 | 27 | // allowed request headers (client can send to server) 28 | AllowedHeaders: []string{ 29 | // general 30 | "content-type", 31 | 32 | // access token 33 | "authorization", 34 | 35 | // pem download authentication 36 | "X-API-Key", "apiKey", 37 | 38 | // conditionals for pem downloads 39 | "if-match", "if-modified-since", "if-none-match", "if-range", "if-unmodified-since", 40 | 41 | // retry tracker for refresh token logic on frontend 42 | "x-no-retry", 43 | }, 44 | 45 | // allowed methods the client can send to the server 46 | AllowedMethods: []string{http.MethodDelete, http.MethodGet, http.MethodHead, 47 | http.MethodPost, http.MethodPut}, 48 | 49 | // headers for client to expose to the cross origin requester (in server response) 50 | ExposedHeaders: []string{ 51 | // general 52 | "content-length", "content-security-policy", "content-type", "referrer-policy", 53 | "strict-transport-security", "vary", "x-content-type-options", "x-frame-options", 54 | 55 | // set name of file when client downloads something (used with pem, zip) 56 | "content-disposition", 57 | 58 | // conditionals for pem downloads 59 | "last-modified", "etag", 60 | }, 61 | }) 62 | 63 | // return custom handlerFunc 64 | return func(w http.ResponseWriter, r *http.Request) *output.JsonError { 65 | // apply cors 66 | c.HandlerFunc(w, r) 67 | 68 | // then next 69 | return next(w, r) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pkg/domain/app/router_global_handlers.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "certwarden-backend/pkg/output" 5 | "errors" 6 | "net/http" 7 | ) 8 | 9 | // handlerNotFound is called when there is not a matching route on the router 10 | func (app *Application) handlerNotFound() http.Handler { 11 | // the base handler function (before middleware) 12 | handlerFunc := func(w http.ResponseWriter, r *http.Request) *output.JsonError { 13 | // return 404 not found 14 | err := app.output.WriteJSON(w, output.JsonErrNotFound(errors.New(r.URL.Path))) 15 | if err != nil { 16 | app.logger.Errorf("failed to write json (%s)", err) 17 | // never return Error since this is already an error 18 | } 19 | return nil 20 | } 21 | 22 | // Add Middleware 23 | 24 | // NO CORS 25 | // no cors info to provide if route is 404 26 | 27 | // Logger / handle custom handler func's error 28 | httpHandlerFunc := middlewareApplyReturnValHandling(handlerFunc, false, app.logger.SugaredLogger, app.output) 29 | 30 | return httpHandlerFunc 31 | } 32 | 33 | // handlerGlobalOptions is called to respond to OPTIONS requests. This is 34 | // particularly important for CORS. 35 | func (app *Application) handlerGlobalOptions() http.Handler { 36 | // the base handler function (before middleware) 37 | handlerFunc := func(w http.ResponseWriter, r *http.Request) *output.JsonError { 38 | // OPTIONS should always return a response to prevent preflight errors 39 | // see: https://stackoverflow.com/questions/52047548/response-for-preflight-does-not-have-http-ok-status-in-angular 40 | 41 | // Note: Disabled as CORS will write the headers automatically 42 | // w.WriteHeader(http.StatusNoContent) 43 | 44 | return nil 45 | } 46 | 47 | // Add Middleware 48 | 49 | // CORS 50 | handlerFunc = middlewareApplyCORS(handlerFunc, app.config.CORSPermittedCrossOrigins) 51 | 52 | // Logger / handle custom handler func's error 53 | httpHandlerFunc := middlewareApplyReturnValHandling(handlerFunc, false, app.logger.SugaredLogger, app.output) 54 | 55 | return httpHandlerFunc 56 | } 57 | -------------------------------------------------------------------------------- /pkg/domain/app/tls.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | // tlsConf returns the app's tls Config for an https web server to use. 10 | func (app *Application) tlsConf() *tls.Config { 11 | tlsConf := &tls.Config{ 12 | // func to return the TLS Cert from app 13 | GetCertificate: app.httpsCert.TlsCertFunc(), 14 | } 15 | 16 | return tlsConf 17 | } 18 | 19 | // HttpsCertificateName returns the db `Name` of the certificate this app is using. 20 | // This allows orders package to call the refresh function whenever the app's 21 | // certificate is reordered. 22 | func (app *Application) HttpsCertificateName() *string { 23 | return app.config.CertificateName 24 | } 25 | 26 | // LoadHttpsCertificate fetches the most recent order for the app's certificate 27 | // and loads it as the app's https certificate. If there is an error, the app 28 | // retains its previous certificate. 29 | func (app *Application) LoadHttpsCertificate() error { 30 | // if not running in https, this is no-op 31 | if !app.IsHttps() { 32 | return errors.New("cannot load https certificate, server is in http mode") 33 | } 34 | 35 | // get order for this app 36 | order, err := app.storage.GetCertNewestValidOrderByName(*app.config.CertificateName) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | // nil check of key 42 | if order.FinalizedKey == nil { 43 | return errors.New("tls key pem is empty") 44 | } 45 | 46 | // nil check of cert pem 47 | if order.Pem == nil { 48 | return errors.New("tls cert pem is empty") 49 | } 50 | 51 | // make tls certificate 52 | tlsCert, err := tls.X509KeyPair([]byte(*order.Pem), []byte(order.FinalizedKey.Pem)) 53 | if err != nil { 54 | return fmt.Errorf("failed to make x509 key pair (%s)", err) 55 | } 56 | 57 | // update certificate 58 | app.httpsCert.Update(&tlsCert) 59 | 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /pkg/domain/app/updater/service.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "certwarden-backend/pkg/output" 5 | "context" 6 | "errors" 7 | "net/http" 8 | "sync" 9 | "time" 10 | 11 | "go.uber.org/zap" 12 | ) 13 | 14 | var errServiceComponent = errors.New("necessary updater service component is missing") 15 | 16 | // App interface is for connecting to the main app 17 | type App interface { 18 | GetAppVersion() string 19 | GetConfigVersion() int 20 | GetLogger() *zap.SugaredLogger 21 | GetHttpClient() *http.Client 22 | GetOutputter() *output.Service 23 | GetShutdownContext() context.Context 24 | GetShutdownWaitGroup() *sync.WaitGroup 25 | } 26 | 27 | // verVersion holds all of the information regarding new version check 28 | // results 29 | type newVersion struct { 30 | available bool 31 | info *versionInfo 32 | lastCheck time.Time 33 | mu sync.RWMutex 34 | } 35 | 36 | // Keys service struct 37 | type Service struct { 38 | logger *zap.SugaredLogger 39 | httpClient *http.Client 40 | output *output.Service 41 | currentVersion string 42 | currentConfigVersion int 43 | checkChannel Channel 44 | newVersion newVersion 45 | } 46 | 47 | // Config holds all of the challenge config 48 | type Config struct { 49 | AutoCheck *bool `yaml:"auto_check"` 50 | Channel *Channel `yaml:"channel"` 51 | } 52 | 53 | // NewService creates a new service 54 | func NewService(app App, cfg *Config) (*Service, error) { 55 | service := new(Service) 56 | 57 | // logger 58 | service.logger = app.GetLogger() 59 | if service.logger == nil { 60 | return nil, errServiceComponent 61 | } 62 | 63 | // http client 64 | service.httpClient = app.GetHttpClient() 65 | 66 | // output service 67 | service.output = app.GetOutputter() 68 | if service.output == nil { 69 | return nil, errServiceComponent 70 | } 71 | 72 | // current version 73 | service.currentVersion = app.GetAppVersion() 74 | service.currentConfigVersion = app.GetConfigVersion() 75 | 76 | // channel to check 77 | service.checkChannel = *cfg.Channel 78 | 79 | // start background auto update check service (if enabled) 80 | if *cfg.AutoCheck { 81 | service.backgroundChecker(app.GetShutdownContext(), app.GetShutdownWaitGroup()) 82 | } 83 | 84 | return service, nil 85 | } 86 | -------------------------------------------------------------------------------- /pkg/domain/authorizations/service.go: -------------------------------------------------------------------------------- 1 | package authorizations 2 | 3 | import ( 4 | "certwarden-backend/pkg/challenges" 5 | "certwarden-backend/pkg/datatypes/safemap" 6 | "certwarden-backend/pkg/domain/acme_servers" 7 | "errors" 8 | 9 | "go.uber.org/zap" 10 | ) 11 | 12 | var errServiceComponent = errors.New("authorizations: necessary service component is missing") 13 | 14 | // App interface is for connecting to the main app 15 | type App interface { 16 | GetLogger() *zap.SugaredLogger 17 | GetChallengesService() *challenges.Service 18 | GetAcmeServerService() *acme_servers.Service 19 | } 20 | 21 | // service struct 22 | type Service struct { 23 | logger *zap.SugaredLogger 24 | acmeServerService *acme_servers.Service 25 | challenges *challenges.Service 26 | authsWorking *safemap.SafeMap[chan struct{}] // tracks auths currently being worked 27 | } 28 | 29 | // NewService creates a new service 30 | func NewService(app App) (service *Service, err error) { 31 | service = new(Service) 32 | 33 | // logger 34 | service.logger = app.GetLogger() 35 | if service.logger == nil { 36 | return nil, errServiceComponent 37 | } 38 | 39 | // acme services 40 | service.acmeServerService = app.GetAcmeServerService() 41 | if service.acmeServerService == nil { 42 | return nil, errServiceComponent 43 | } 44 | 45 | // challenge solver 46 | service.challenges = app.GetChallengesService() 47 | if service.challenges == nil { 48 | return nil, errServiceComponent 49 | } 50 | 51 | // initialize working 52 | service.authsWorking = safemap.NewSafeMap[chan struct{}]() 53 | 54 | return service, nil 55 | } 56 | -------------------------------------------------------------------------------- /pkg/domain/certificates/csr.go: -------------------------------------------------------------------------------- 1 | package certificates 2 | 3 | import ( 4 | "certwarden-backend/pkg/domain/private_keys/key_crypto" 5 | "crypto/rand" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | ) 9 | 10 | // MakeCsrDer generates the CSR bytes for ACME to POST To a Finalize URL 11 | func (cert *Certificate) MakeCsrDer() (csr []byte, err error) { 12 | // omit empty fields 13 | org := []string{} 14 | if cert.Organization != "" { 15 | org = append(org, cert.Organization) 16 | } 17 | 18 | ou := []string{} 19 | if cert.OrganizationalUnit != "" { 20 | ou = append(ou, cert.OrganizationalUnit) 21 | } 22 | 23 | country := []string{} 24 | if cert.Country != "" { 25 | country = append(country, cert.Country) 26 | } 27 | 28 | province := []string{} 29 | if cert.State != "" { 30 | province = append(province, cert.State) 31 | } 32 | 33 | locality := []string{} 34 | if cert.City != "" { 35 | locality = append(locality, cert.City) 36 | } 37 | 38 | // create Subject 39 | subj := pkix.Name{ 40 | CommonName: cert.Subject, 41 | Organization: org, 42 | OrganizationalUnit: ou, 43 | Country: country, 44 | Province: province, 45 | Locality: locality, 46 | // unused: StreetAddress, PostalCode []string 47 | // unused: SerialNumber string 48 | // unused: Names, ExtraNames []AttributeTypeAndValue 49 | } 50 | 51 | // convert any extra extensions to proper pkix obj 52 | extraExts := []pkix.Extension{} 53 | for i := range cert.CSRExtraExtensions { 54 | extraExts = append(extraExts, cert.CSRExtraExtensions[i].Extension) 55 | } 56 | 57 | // CSR template to create CSR from 58 | template := x509.CertificateRequest{ 59 | SignatureAlgorithm: cert.CertificateKey.Algorithm.CsrSigningAlg(), 60 | Subject: subj, 61 | DNSNames: append([]string{cert.Subject}, cert.SubjectAltNames...), 62 | // unused: EmailAddresses, IPAddresses, URIs, Attributes (deprecated) 63 | ExtraExtensions: extraExts, 64 | } 65 | 66 | // cert's private key for signing 67 | certKey, err := key_crypto.PemStringToKey(cert.CertificateKey.Pem, cert.CertificateKey.Algorithm) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | // create CSR 73 | csr, err = x509.CreateCertificateRequest(rand.Reader, &template, certKey) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | return csr, nil 79 | } 80 | -------------------------------------------------------------------------------- /pkg/domain/download/errors.go: -------------------------------------------------------------------------------- 1 | package download 2 | 3 | import "errors" 4 | 5 | var ( 6 | errBlankApiKey = errors.New("no apikey found") 7 | errWrongApiKey = errors.New("apikey is incorrect") 8 | 9 | errApiKeyFromUrlDisallowed = errors.New("apikey found in url but not allowed") 10 | 11 | errApiDisabled = errors.New("download via api is disabled") 12 | 13 | errFinalizedKeyMissing = errors.New("cert has a valid order but the finalized key is missing") 14 | 15 | errNoPem = errors.New("pem is blank") 16 | ) 17 | -------------------------------------------------------------------------------- /pkg/domain/download/fetch_key.go: -------------------------------------------------------------------------------- 1 | package download 2 | 3 | import ( 4 | "certwarden-backend/pkg/domain/private_keys" 5 | "certwarden-backend/pkg/output" 6 | "certwarden-backend/pkg/storage" 7 | "errors" 8 | "time" 9 | ) 10 | 11 | // getKey returns the private key if the apiKey matches 12 | // the requested key. It also checks the apiKeyViaUrl property if 13 | // the client is making a request with the apiKey in the Url. 14 | func (service *Service) getKey(keyName string, apiKey string, apiKeyViaUrl bool) (private_keys.Key, *output.JsonError) { 15 | // if apiKey is blank, definitely unauthorized 16 | if apiKey == "" { 17 | service.logger.Debug(errBlankApiKey) 18 | return private_keys.Key{}, output.JsonErrUnauthorized 19 | } 20 | 21 | // get the key from storage 22 | key, err := service.storage.GetOneKeyByName(keyName) 23 | if err != nil { 24 | // special error case for no record found 25 | if errors.Is(err, storage.ErrNoRecord) { 26 | service.logger.Debug(err) 27 | // exclude specific error since not authenticated 28 | return private_keys.Key{}, output.JsonErrNotFound(nil) 29 | } else { 30 | service.logger.Error(err) 31 | // exclude specific error since not authenticated 32 | return private_keys.Key{}, output.JsonErrStorageGeneric(nil) 33 | } 34 | } 35 | 36 | // if key is disabled via API, error 37 | if key.ApiKeyDisabled { 38 | service.logger.Debug(errApiDisabled) 39 | return private_keys.Key{}, output.JsonErrUnauthorized 40 | } 41 | 42 | // if apiKey came from URL, and key does not support this, error 43 | if apiKeyViaUrl && !key.ApiKeyViaUrl { 44 | service.logger.Debug(errApiKeyFromUrlDisallowed) 45 | return private_keys.Key{}, output.JsonErrUnauthorized 46 | } 47 | 48 | // verify apikey matches private key's apiKey (new or old) 49 | if (apiKey != key.ApiKey) && (apiKey != key.ApiKeyNew) { 50 | service.logger.Debug(errWrongApiKey) 51 | return private_keys.Key{}, output.JsonErrUnauthorized 52 | } 53 | 54 | // before return, update key last access, dont fail our though if this step fails, just log error 55 | err = service.storage.PutKeyLastAccess(key.ID, time.Now().Unix()) 56 | if err != nil { 57 | service.logger.Errorf("download: failed to update key (id: %d) last access time (%s)", key.ID, err) 58 | } 59 | 60 | // return key 61 | return key, nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/domain/download/header.go: -------------------------------------------------------------------------------- 1 | package download 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | // apiKeyHeaderNames are the potential header names that will be checked 9 | // for the api key 10 | var apiKeyHeaderNames = []string{ 11 | "apiKey", 12 | "X-API-Key", 13 | } 14 | 15 | // getApiKeyFromHeader gets the api key from approved headers and also 16 | // modifies ResponseWriter to include the Vary header re: api key 17 | func getApiKeyFromHeader(w http.ResponseWriter, r *http.Request) (apiKeyHeaderValue string) { 18 | // all api key fields should be included in Vary 19 | varyCanonicalVals := []string{} 20 | for _, varyVal := range apiKeyHeaderNames { 21 | // use canonical names 22 | varyCanonicalVals = append(varyCanonicalVals, http.CanonicalHeaderKey(varyVal)) 23 | } 24 | varyValsString := strings.Join(varyCanonicalVals, ", ") 25 | 26 | // add to Vary 27 | w.Header().Add("Vary", varyValsString) 28 | 29 | // find api key value 30 | for _, headerName := range apiKeyHeaderNames { 31 | // set value of header found 32 | if r.Header.Get(headerName) != "" { 33 | return r.Header.Get(headerName) 34 | } 35 | } 36 | 37 | return "" 38 | } 39 | -------------------------------------------------------------------------------- /pkg/domain/download/out_certificates.go: -------------------------------------------------------------------------------- 1 | package download 2 | 3 | import ( 4 | "certwarden-backend/pkg/output" 5 | "net/http" 6 | 7 | "github.com/julienschmidt/httprouter" 8 | ) 9 | 10 | // DownloadCertViaHeader is the handler to write a cert to the client 11 | // if the proper apiKey is provided via header (standard method) 12 | func (service *Service) DownloadCertViaHeader(w http.ResponseWriter, r *http.Request) *output.JsonError { 13 | // get name from request 14 | params := httprouter.ParamsFromContext(r.Context()) 15 | certName := params.ByName("name") 16 | 17 | // get apiKey from header 18 | apiKey := getApiKeyFromHeader(w, r) 19 | 20 | // fetch the cert's newest order using the apiKey 21 | order, err := service.getCertNewestValidOrder(certName, apiKey, false, false) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | // return pem file to client 27 | service.output.WritePem(w, r, order) 28 | 29 | return nil 30 | } 31 | 32 | // DownloadCertViaUrl is the handler to write a cert to the client 33 | // if the proper apiKey is provided via URL (NOT recommended - only implemented 34 | // to support clients that can't specify the apiKey header) 35 | func (service *Service) DownloadCertViaUrl(w http.ResponseWriter, r *http.Request) *output.JsonError { 36 | // get cert name & apiKey 37 | params := httprouter.ParamsFromContext(r.Context()) 38 | certName := params.ByName("name") 39 | 40 | apiKey := getApiKeyFromParams(params) 41 | 42 | // fetch the cert's newest order using the apiKey 43 | order, err := service.getCertNewestValidOrder(certName, apiKey, true, false) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | // return pem file to client 49 | service.output.WritePem(w, r, order) 50 | 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/domain/download/out_private_certs.go: -------------------------------------------------------------------------------- 1 | package download 2 | 3 | import ( 4 | "certwarden-backend/pkg/domain/orders" 5 | "certwarden-backend/pkg/output" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/julienschmidt/httprouter" 11 | ) 12 | 13 | // modified Order to allow implementation of custom out functions 14 | // to properly output the desired content 15 | type privateCertificate orders.Order 16 | 17 | // privateCertificate Output Methods 18 | 19 | func (pc privateCertificate) FilenameNoExt() string { 20 | return fmt.Sprintf("%s.certkey", pc.Certificate.Name) 21 | } 22 | 23 | func (pc privateCertificate) Modtime() time.Time { 24 | return orders.Order(pc).Modtime() 25 | } 26 | 27 | func (pc privateCertificate) PemContent() string { 28 | keyPem := pc.FinalizedKey.PemContent() 29 | // don't include the cert chain 30 | certPem := orders.Order(pc).PemContentNoChain() 31 | 32 | // append key + LF + cert 33 | return keyPem + string([]byte{10}) + certPem 34 | } 35 | 36 | // end privateCertificate Output Methods 37 | 38 | // DownloadPrivateCertViaHeader 39 | func (service *Service) DownloadPrivateCertViaHeader(w http.ResponseWriter, r *http.Request) *output.JsonError { 40 | // get cert name 41 | params := httprouter.ParamsFromContext(r.Context()) 42 | certName := params.ByName("name") 43 | 44 | // get apiKey from header 45 | apiKeysCombined := getApiKeyFromHeader(w, r) 46 | 47 | // fetch the private cert 48 | order, err := service.getCertNewestValidOrder(certName, apiKeysCombined, false, true) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | // return pem file to client 54 | privCert := privateCertificate(order) 55 | service.output.WritePem(w, r, privCert) 56 | 57 | return nil 58 | } 59 | 60 | // DownloadPrivateCertViaUrl 61 | func (service *Service) DownloadPrivateCertViaUrl(w http.ResponseWriter, r *http.Request) *output.JsonError { 62 | // get cert name & apiKey 63 | params := httprouter.ParamsFromContext(r.Context()) 64 | certName := params.ByName("name") 65 | 66 | apiKeysCombined := getApiKeyFromParams(params) 67 | 68 | // fetch the private cert 69 | order, err := service.getCertNewestValidOrder(certName, apiKeysCombined, true, true) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | // return pem file to client 75 | privCert := privateCertificate(order) 76 | service.output.WritePem(w, r, privCert) 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /pkg/domain/download/out_private_keys.go: -------------------------------------------------------------------------------- 1 | package download 2 | 3 | import ( 4 | "certwarden-backend/pkg/output" 5 | "net/http" 6 | 7 | "github.com/julienschmidt/httprouter" 8 | ) 9 | 10 | // DownloadKeyViaHeader is the handler to write a private key to the client 11 | // if the proper apiKey is provided via header (standard method) 12 | func (service *Service) DownloadKeyViaHeader(w http.ResponseWriter, r *http.Request) *output.JsonError { 13 | // get key name 14 | params := httprouter.ParamsFromContext(r.Context()) 15 | keyName := params.ByName("name") 16 | 17 | // get apiKey from header 18 | apiKey := getApiKeyFromHeader(w, r) 19 | 20 | // fetch the key using the apiKey 21 | key, err := service.getKey(keyName, apiKey, false) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | // return pem file to client 27 | service.output.WritePem(w, r, key) 28 | 29 | return nil 30 | } 31 | 32 | // DownloadKeyViaUrl is the handler to write a private key to the client 33 | // if the proper apiKey is provided via URL (NOT recommended - only implemented 34 | // to support clients that can't specify the apiKey header) 35 | func (service *Service) DownloadKeyViaUrl(w http.ResponseWriter, r *http.Request) *output.JsonError { 36 | // get key name & apiKey 37 | params := httprouter.ParamsFromContext(r.Context()) 38 | keyName := params.ByName("name") 39 | 40 | apiKey := getApiKeyFromParams(params) 41 | 42 | // fetch the key using the apiKey 43 | key, err := service.getKey(keyName, apiKey, true) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | // return pem file to client 49 | service.output.WritePem(w, r, key) 50 | 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/domain/download/service.go: -------------------------------------------------------------------------------- 1 | package download 2 | 3 | import ( 4 | "certwarden-backend/pkg/domain/certificates" 5 | "certwarden-backend/pkg/domain/orders" 6 | "certwarden-backend/pkg/domain/private_keys" 7 | "certwarden-backend/pkg/output" 8 | "errors" 9 | 10 | "go.uber.org/zap" 11 | ) 12 | 13 | var errServiceComponent = errors.New("necessary download service component is missing") 14 | 15 | // App interface is for connecting to the main app 16 | type App interface { 17 | GetLogger() *zap.SugaredLogger 18 | GetOutputter() *output.Service 19 | GetDownloadStorage() Storage 20 | } 21 | 22 | // Storage interface for storage functions 23 | type Storage interface { 24 | GetOneKeyByName(name string) (private_keys.Key, error) 25 | 26 | GetOneCertByName(name string) (cert certificates.Certificate, err error) 27 | 28 | GetCertNewestValidOrderByName(certName string) (order orders.Order, err error) 29 | 30 | PutKeyLastAccess(keyId int, unixLastAccessTime int64) (err error) 31 | PutCertLastAccess(certId int, unixLastAccessTime int64) (err error) 32 | } 33 | 34 | // Keys service struct 35 | type Service struct { 36 | logger *zap.SugaredLogger 37 | output *output.Service 38 | storage Storage 39 | } 40 | 41 | // NewService creates a new private_key service 42 | func NewService(app App) (*Service, error) { 43 | service := new(Service) 44 | 45 | // logger 46 | service.logger = app.GetLogger() 47 | if service.logger == nil { 48 | return nil, errServiceComponent 49 | } 50 | 51 | // output service 52 | service.output = app.GetOutputter() 53 | if service.output == nil { 54 | return nil, errServiceComponent 55 | } 56 | 57 | // storage 58 | service.storage = app.GetDownloadStorage() 59 | if service.storage == nil { 60 | return nil, errServiceComponent 61 | } 62 | 63 | return service, nil 64 | } 65 | -------------------------------------------------------------------------------- /pkg/domain/download/viaurl.go: -------------------------------------------------------------------------------- 1 | package download 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/julienschmidt/httprouter" 7 | ) 8 | 9 | // getApiKeyFromParams parses the apiKey wild card param to ensure only 10 | // the apiKey is returned. The remainder of the param is discarded. 11 | func getApiKeyFromParams(params httprouter.Params) (apiKey string) { 12 | // get the wildcard apikey param 13 | apiKey = params.ByName("apiKey") 14 | 15 | // split apiKey at slashes (/) 16 | pieces := strings.Split(apiKey, "/") 17 | 18 | // the param always starts with a slash, so second piece is the apiKey 19 | return pieces[1] 20 | } 21 | -------------------------------------------------------------------------------- /pkg/domain/orders/fulfilling.go: -------------------------------------------------------------------------------- 1 | package orders 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // orderFulfillJob represents a job that fulfills ACME orders, including all 9 | // variables needed to actually Do the job 10 | type orderFulfillJob struct { 11 | service *Service 12 | 13 | addedToQueue time.Time 14 | highPriority bool 15 | orderID int 16 | } 17 | 18 | // makeFulfillingJob makes an orderFulfillJob 19 | func (service *Service) makeFulfillingJob(orderID int, highPriority bool) (*orderFulfillJob, error) { 20 | // get order (i.e. validate it exists) 21 | order, err := service.storage.GetOneOrder(orderID) 22 | if err != nil { 23 | return nil, fmt.Errorf("orders: fulfilling: failed to make fulfill job for order id %d (%s)", orderID, err) 24 | } 25 | 26 | // cant fulfill if already in a final state 27 | if order.Status == "valid" || order.Status == "invalid" { 28 | return nil, fmt.Errorf("orders: fulfilling: failed to make fulfill job for order id %d (already in final state %s)", orderID, order.Status) 29 | } 30 | 31 | return &orderFulfillJob{ 32 | service: service, 33 | 34 | addedToQueue: time.Now(), 35 | highPriority: highPriority, 36 | orderID: orderID, 37 | }, nil 38 | } 39 | 40 | // Description implements part of the Job interface and returns a string 41 | // that will be used for logging purposes 42 | func (j *orderFulfillJob) Description() string { 43 | return fmt.Sprintf("order id: %d", j.orderID) 44 | } 45 | 46 | // Equal implements part of the Job interface to determine if two jobs 47 | // should be considered the same job 48 | func (j *orderFulfillJob) Equal(j2 *orderFulfillJob) bool { 49 | return j != nil && j2 != nil && j.orderID == j2.orderID 50 | } 51 | 52 | // IsHighPriority implements Job interface priority func 53 | func (j *orderFulfillJob) IsHighPriority() bool { 54 | return j.highPriority 55 | } 56 | -------------------------------------------------------------------------------- /pkg/domain/orders/fulfilling_add.go: -------------------------------------------------------------------------------- 1 | package orders 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // fulfillOrder queues the specified order ID with the specified priority level 8 | // for fulfillment of that order with the ACME server 9 | func (service *Service) fulfillOrder(orderID int, isHighPriority bool) (err error) { 10 | // make job 11 | newJob, err := service.makeFulfillingJob(orderID, isHighPriority) 12 | if err != nil { 13 | return err 14 | } 15 | 16 | // add to the Job Manager 17 | err = service.orderFulfilling.AddJob(newJob) 18 | if err != nil { 19 | return fmt.Errorf("orders: fulfilling: failed to add order id %d (%s)", orderID, err) 20 | } 21 | 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /pkg/domain/orders/fulfilling_pem_processing.go: -------------------------------------------------------------------------------- 1 | package orders 2 | 3 | import ( 4 | "certwarden-backend/pkg/acme" 5 | "time" 6 | ) 7 | 8 | // this relates to the order's issued certificate, not to be conflated with the 'certificates' 9 | // package 10 | 11 | // CertPayload is the data to store for an issued certificate 12 | type CertPayload struct { 13 | AcmeCert *acme.Certificate 14 | UpdatedAt time.Time 15 | } 16 | 17 | // savePemChain calls a func to determine the valid from and to dates for the issued pem chain 18 | // and then saves the pem chain and valid dates to storage 19 | func (j *orderFulfillJob) saveAcmeCert(orderId int, cert *acme.Certificate) (err error) { 20 | // payload to save 21 | payload := &CertPayload{ 22 | AcmeCert: cert, 23 | UpdatedAt: time.Now(), 24 | } 25 | 26 | // save to storage 27 | err = j.service.storage.UpdateOrderCert(orderId, payload) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/domain/orders/post_process.go: -------------------------------------------------------------------------------- 1 | package orders 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // postProcessJob represents a post processing job, including all variables needed 9 | // to actually Do the job 10 | type postProcessJob struct { 11 | service *Service 12 | 13 | addedToQueue time.Time 14 | highPriority bool 15 | orderID int 16 | certificateID int 17 | } 18 | 19 | // makeFulfillingJob makes an orderFulfillJob 20 | func (service *Service) makePostProcessJob(orderID int, highPriority bool) (*postProcessJob, error) { 21 | // get order 22 | order, err := service.storage.GetOneOrder(orderID) 23 | if err != nil { 24 | return nil, fmt.Errorf("orders: post processing: failed to make post process job for order id %d (%s)", orderID, err) 25 | } 26 | 27 | // fail add if order isn't valid 28 | if order.Status != "valid" { 29 | return nil, fmt.Errorf("orders: post processing: failed to make post process job for order id %d (status is not 'valid')", orderID) 30 | } 31 | 32 | // confirm order actually has post processing to do 33 | if !order.hasPostProcessingToDo() { 34 | return nil, fmt.Errorf("orders: post processing: failed to make post process job for order id %d (certificate %s has no post processing configured)", orderID, order.Certificate.Name) 35 | } 36 | 37 | return &postProcessJob{ 38 | service: service, 39 | 40 | addedToQueue: time.Now(), 41 | highPriority: highPriority, 42 | orderID: orderID, 43 | certificateID: order.Certificate.ID, 44 | }, nil 45 | } 46 | 47 | // Description implements part of the Job interface and returns a string 48 | // that will be used for logging purposes 49 | func (j *postProcessJob) Description() string { 50 | return fmt.Sprintf("certificate id: %d, order id: %d", j.certificateID, j.orderID) 51 | } 52 | 53 | // Equal implements part of the Job interface to determine if two jobs 54 | // should be considered the same job 55 | func (j *postProcessJob) Equal(j2 *postProcessJob) bool { 56 | return j != nil && j2 != nil && (j.orderID == j2.orderID || j.certificateID == j2.certificateID) 57 | } 58 | 59 | // IsHighPriority implements Job interface priority func 60 | func (j *postProcessJob) IsHighPriority() bool { 61 | return j.highPriority 62 | } 63 | -------------------------------------------------------------------------------- /pkg/domain/orders/post_process_add.go: -------------------------------------------------------------------------------- 1 | package orders 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // postProcess queues a post processing job for the specified order ID with the specified 8 | // priority level 9 | func (service *Service) postProcess(orderID int, isHighPriority bool) (err error) { 10 | // make job 11 | newJob, err := service.makePostProcessJob(orderID, isHighPriority) 12 | if err != nil { 13 | return err 14 | } 15 | 16 | // add to the Job Manager 17 | err = service.postProcessing.AddJob(newJob) 18 | if err != nil { 19 | return fmt.Errorf("orders: post processing: failed to add order id %d (%s)", orderID, err) 20 | } 21 | 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /pkg/domain/orders/post_process_do.go: -------------------------------------------------------------------------------- 1 | package orders 2 | 3 | // Do actually runs the post processing task(s) 4 | func (j *postProcessJob) Do(workerID int) { 5 | // get order 6 | order, err := j.service.storage.GetOneOrder(j.orderID) 7 | if err != nil { 8 | j.service.logger.Errorf("orders: ost processing worker %d: failed to get order %d from db for post processing (%s)", workerID, j.orderID, err) 9 | return // done, failed 10 | } 11 | 12 | // run client post processing 13 | j.doClientPostProcess(order, workerID) 14 | 15 | // run command post processing 16 | j.doScriptOrBinaryPostProcess(order, workerID) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/domain/private_keys/key_crypto/algorithm.go: -------------------------------------------------------------------------------- 1 | package key_crypto 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/json" 6 | "errors" 7 | ) 8 | 9 | var errUnsupportedAlgorithm = errors.New("unsupported algorithm") 10 | 11 | // define Algorithm 12 | type Algorithm int 13 | 14 | // define available Algorithms 15 | const ( 16 | UnknownAlgorithm Algorithm = iota 17 | 18 | rsa2048 19 | rsa3072 20 | rsa4096 21 | ecdsap256 22 | ecdsap384 23 | ) 24 | 25 | // Algorithm custom JSON Marshal (turns the Algorithm into exportable AlgorithmDetails 26 | // for output) 27 | func (alg Algorithm) MarshalJSON() (data []byte, err error) { 28 | // get alg details 29 | details := alg.details() 30 | 31 | // put the exportable details into an exportable struct 32 | output := struct { 33 | StorageValue string `json:"value"` 34 | Name string `json:"name"` 35 | }{ 36 | StorageValue: details.storageValue, 37 | Name: details.name, 38 | } 39 | 40 | // return details marshalled 41 | return json.Marshal(output) 42 | } 43 | 44 | // custom UnmarshalJSON not needed at present 45 | 46 | // details returns the full details for the Algorithm. 47 | func (alg Algorithm) details() algorithmDetails { 48 | for i := range keyAlgorithmDetails { 49 | if alg == keyAlgorithmDetails[i].algorithm { 50 | return keyAlgorithmDetails[i] 51 | } 52 | } 53 | 54 | // no details exist 55 | return algorithmDetails{} 56 | } 57 | 58 | // CsrSigningAlg returns the x509.SignatureAlgorithm for the Algorithm. 59 | func (alg Algorithm) CsrSigningAlg() x509.SignatureAlgorithm { 60 | return alg.details().csrSignatureAlgorithm 61 | } 62 | 63 | // StorageValue returns the storage value of the Algorithm 64 | func (alg Algorithm) StorageValue() string { 65 | return alg.details().storageValue 66 | } 67 | -------------------------------------------------------------------------------- /pkg/domain/private_keys/service.go: -------------------------------------------------------------------------------- 1 | package private_keys 2 | 3 | import ( 4 | "certwarden-backend/pkg/output" 5 | "certwarden-backend/pkg/pagination_sort" 6 | "errors" 7 | 8 | "go.uber.org/zap" 9 | ) 10 | 11 | var errServiceComponent = errors.New("necessary key service component is missing") 12 | 13 | // App interface is for connecting to the main app 14 | type App interface { 15 | GetLogger() *zap.SugaredLogger 16 | GetOutputter() *output.Service 17 | GetKeyStorage() Storage 18 | } 19 | 20 | // Storage interface for storage functions 21 | type Storage interface { 22 | GetAllKeys(q pagination_sort.Query) (keys []Key, totalRows int, err error) 23 | GetOneKeyById(id int) (Key, error) 24 | GetOneKeyByName(name string) (Key, error) 25 | 26 | PostNewKey(NewPayload) (Key, error) 27 | 28 | PutKeyUpdate(UpdatePayload) (Key, error) 29 | PutKeyApiKey(keyId int, apiKey string, updateTimeUnix int) (err error) 30 | PutKeyNewApiKey(keyId int, newApiKey string, updateTimeUnix int) error 31 | 32 | DeleteKey(int) error 33 | 34 | GetAvailableKeys() ([]Key, error) 35 | KeyInUse(id int) (inUse bool, err error) 36 | } 37 | 38 | // Keys service struct 39 | type Service struct { 40 | logger *zap.SugaredLogger 41 | output *output.Service 42 | storage Storage 43 | } 44 | 45 | // NewService creates a new private_key service 46 | func NewService(app App) (*Service, error) { 47 | service := new(Service) 48 | 49 | // logger 50 | service.logger = app.GetLogger() 51 | if service.logger == nil { 52 | return nil, errServiceComponent 53 | } 54 | 55 | // output service 56 | service.output = app.GetOutputter() 57 | if service.output == nil { 58 | return nil, errServiceComponent 59 | } 60 | 61 | // storage 62 | service.storage = app.GetKeyStorage() 63 | if service.storage == nil { 64 | return nil, errServiceComponent 65 | } 66 | 67 | return service, nil 68 | } 69 | -------------------------------------------------------------------------------- /pkg/output/errors.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // JsonError is the standardized error structure, it is the same as a regular message but also 8 | // implements the Error() interface 9 | type JsonError JsonResponse 10 | 11 | // HttpStatusCode() implements the jsonData interface 12 | func (je *JsonError) HttpStatusCode() int { 13 | return je.StatusCode 14 | } 15 | 16 | // Error() implements the error interface 17 | func (je JsonError) Error() string { 18 | return fmt.Sprintf("%d: %s", je.StatusCode, je.Message) 19 | } 20 | 21 | // 22 | // Funcs to make various Output Errors 23 | // 24 | 25 | // generic 26 | func JsonErrNotFound(err error) *JsonError { 27 | return &JsonError{ 28 | StatusCode: 404, 29 | Message: fmt.Sprintf("error: not found (%s)", err), 30 | } 31 | } 32 | 33 | func JsonErrInternal(err error) *JsonError { 34 | return &JsonError{ 35 | StatusCode: 500, 36 | Message: fmt.Sprintf("error: internal (%s)", err), 37 | } 38 | } 39 | 40 | var JsonErrUnauthorized = &JsonError{StatusCode: 401, Message: "unauthorized"} 41 | 42 | // storage 43 | func JsonErrStorageGeneric(err error) *JsonError { 44 | return &JsonError{ 45 | StatusCode: 500, 46 | Message: fmt.Sprintf("error: storage error (%s)", err), 47 | } 48 | } 49 | 50 | func JsonErrDeleteInUse(recordType string) *JsonError { 51 | return &JsonError{ 52 | StatusCode: 409, 53 | Message: fmt.Sprintf("error: record (%s) in use, can't delete", recordType), 54 | } 55 | } 56 | 57 | // write 58 | func JsonErrWriteJsonError(err error) *JsonError { 59 | return &JsonError{ 60 | StatusCode: 500, 61 | Message: fmt.Sprintf("error: json response write failed (%s)", err), 62 | } 63 | } 64 | 65 | // validation 66 | func JsonErrValidationFailed(err error) *JsonError { 67 | return &JsonError{ 68 | StatusCode: 400, 69 | Message: fmt.Sprintf("error: request validation (param or payload) invalid (%s)", err), 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pkg/output/file.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | // outFile is a common interface that all file outputs share 11 | type outFile interface { 12 | FilenameNoExt() string 13 | Modtime() time.Time 14 | } 15 | 16 | // outFileObj is the object the public functions should populate to use the common 17 | // writeFile function 18 | type outFileObj struct { 19 | filename string 20 | content []byte 21 | httpContentType string 22 | modTime time.Time 23 | eTag string 24 | } 25 | 26 | // writeNoCacheFile is a generic file output function that is used by other public file output 27 | // functions; it also includes a `no-store` cache header 28 | func (service *Service) writeFile(w http.ResponseWriter, r *http.Request, file outFileObj) { 29 | // get filename and log for auditing 30 | filename := file.filename 31 | service.logger.Debugf("writing file %s to client %s", filename, r.RemoteAddr) 32 | 33 | // get pem content and convert to Reader 34 | contentReader := bytes.NewReader(file.content) 35 | 36 | // Set Content-Type and Content-Disposition headers explicitly 37 | w.Header().Set("Content-Type", file.httpContentType) 38 | w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 39 | 40 | // set eTag if there is one 41 | if file.eTag != "" { 42 | w.Header().Set("ETag", file.eTag) 43 | } 44 | 45 | // write no-store Cache header - these files could change at any time (e.g., a renewal) 46 | w.Header().Set("Cache-Control", "no-store") 47 | 48 | // do not write HTTP Status, ServeContent will handle this 49 | 50 | // ServeContent (technically fielname is not needed here since Content-Type is set explicitly above) 51 | http.ServeContent(w, r, filename, file.modTime, contentReader) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/output/file_pem.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // PemObject is an interface for objects that can be written to the client as 10 | // PEM data. It contains all methods needed to do this. 11 | type PemObject interface { 12 | outFile 13 | PemContent() string 14 | } 15 | 16 | // WritePem sends an object supporting PEM output to the client as the appropriate application type 17 | func (service *Service) WritePem(w http.ResponseWriter, r *http.Request, obj PemObject) { 18 | pemContent := []byte(obj.PemContent()) 19 | 20 | file := outFileObj{ 21 | filename: obj.FilenameNoExt() + ".pem", 22 | content: pemContent, 23 | httpContentType: "application/x-pem-file", 24 | modTime: obj.Modtime(), 25 | eTag: fmt.Sprintf("\"%x\"", sha1.Sum(pemContent)), 26 | } 27 | 28 | service.writeFile(w, r, file) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/output/file_pfx.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // PfxObject is an interface for objects that can be written to the client as 10 | // PFX data. It contains all methods needed to do this. 11 | type PfxObject interface { 12 | outFile 13 | PfxContent(legacy3DES bool) ([]byte, error) 14 | } 15 | 16 | // WritePfx sends an object supporting PFX output to the client as the appropriate application type 17 | func (service *Service) WritePfx(w http.ResponseWriter, r *http.Request, obj PfxObject, legacy3DES bool) error { 18 | pfxContent, err := obj.PfxContent(legacy3DES) 19 | if err != nil { 20 | service.logger.Errorf("error generating pfx (%s)", err) 21 | return err 22 | } 23 | 24 | file := outFileObj{ 25 | filename: obj.FilenameNoExt() + ".pfx", 26 | content: pfxContent, 27 | httpContentType: "application/x-pkcs12", 28 | modTime: obj.Modtime(), 29 | eTag: fmt.Sprintf("\"%x\"", sha1.Sum(pfxContent)), 30 | } 31 | 32 | service.writeFile(w, r, file) 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /pkg/output/file_zip.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | // WriteZip sends a zip file with the specified filename using the supplied content 9 | func (service *Service) WriteZip(w http.ResponseWriter, r *http.Request, filenameNoExt string, zipContent []byte) { 10 | file := outFileObj{ 11 | filename: filenameNoExt + ".zip", 12 | content: zipContent, 13 | httpContentType: "application/zip", 14 | modTime: time.Time{}, 15 | // no eTag 16 | } 17 | 18 | service.writeFile(w, r, file) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/output/json.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "go.uber.org/zap/zapcore" 8 | ) 9 | 10 | // JsonResponse is the standard response to clients 11 | type JsonResponse struct { 12 | StatusCode int `json:"status_code"` 13 | Message string `json:"message"` 14 | } 15 | 16 | func (jr *JsonResponse) HttpStatusCode() int { 17 | return jr.StatusCode 18 | } 19 | 20 | type jsonData interface { 21 | HttpStatusCode() int 22 | } 23 | 24 | // WriteJSON marshalls data and then writes it to the ResponseWriter. An error is 25 | // returned if writing failed. 26 | func (service *Service) WriteJSON(w http.ResponseWriter, data jsonData) error { 27 | // marshalling: make it pretty if doing debug logging 28 | var jsonBytes []byte 29 | var err error 30 | if service.logger.Level() == zapcore.DebugLevel { 31 | jsonBytes, err = json.MarshalIndent(data, "", "\t") 32 | } else { 33 | jsonBytes, err = json.Marshal(data) 34 | } 35 | if err != nil { 36 | service.logger.Errorf("error marshalling json (%s)", err) 37 | return err 38 | } 39 | 40 | w.Header().Set("Content-Type", "application/json") 41 | w.WriteHeader(data.HttpStatusCode()) 42 | 43 | _, err = w.Write(jsonBytes) 44 | if err != nil { 45 | service.logger.Errorf("error writing json (%s)", err) 46 | return err 47 | } 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /pkg/output/redaction.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | // RedactString removes the middle portion of a string and returns only the first and last 4 | // characters separated by asterisks. 5 | func RedactString(s string) string { 6 | // if s is less than or equal to 4 characters, do not redact; nothing sensitive 7 | // should ever be this small 8 | if len(s) <= 5 { 9 | return s 10 | } 11 | 12 | // return first 3 + asterisks + last 2 13 | return string(s[:2]) + "************" + string(s[len(s)-2:]) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/output/service.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "errors" 5 | 6 | "go.uber.org/zap" 7 | ) 8 | 9 | var errServiceComponent = errors.New("necessary output service component is missing") 10 | 11 | type App interface { 12 | GetLogger() *zap.SugaredLogger 13 | } 14 | 15 | type Service struct { 16 | logger *zap.SugaredLogger 17 | } 18 | 19 | // NewService creates a new private_key service 20 | func NewService(app App) (*Service, error) { 21 | service := new(Service) 22 | 23 | // logger 24 | service.logger = app.GetLogger() 25 | if service.logger == nil { 26 | return nil, errServiceComponent 27 | } 28 | 29 | return service, nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/randomness/backoff.go: -------------------------------------------------------------------------------- 1 | package randomness 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/cenkalti/backoff/v4" 8 | ) 9 | 10 | // BackoffACME returns a backoff to be used for ACME operations while waiting for a 11 | // remote ACME service to finish working on some task 12 | // RFC8555 mentions 5 - 10 seconds when discussing waiting on challenges, so use 7 13 | // seconds as the starting point 14 | func BackoffACME(shutdownCtx context.Context) backoff.BackOffContext { 15 | bo := backoff.NewExponentialBackOff() 16 | bo.InitialInterval = 7 * time.Second 17 | bo.RandomizationFactor = 0.4 18 | bo.Multiplier = 1.4 19 | bo.MaxInterval = 60 * time.Second 20 | bo.MaxElapsedTime = 30 * time.Minute 21 | 22 | boWithContext := backoff.WithContext(bo, shutdownCtx) 23 | 24 | return boWithContext 25 | } 26 | -------------------------------------------------------------------------------- /pkg/storage/errors.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import "errors" 4 | 5 | // errors in generic storage package so there are no dependencies on sql or 6 | // sql error types 7 | 8 | var ( 9 | ErrInUse = errors.New("record in use") 10 | ErrNoRecord = errors.New("no such record found in storage") 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/accounts.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "certwarden-backend/pkg/domain/acme_accounts" 5 | ) 6 | 7 | // accountDb is a single acme account, as database table fields 8 | // corresponds to acme_accounts.Account 9 | type accountDb struct { 10 | id int 11 | name string 12 | description string 13 | accountServerDb acmeServerDb 14 | accountKeyDb keyDb 15 | status string 16 | email string 17 | acceptedTos bool 18 | createdAt int 19 | updatedAt int 20 | kid string 21 | } 22 | 23 | func (acct accountDb) toAccount() acme_accounts.Account { 24 | return acme_accounts.Account{ 25 | ID: acct.id, 26 | Name: acct.name, 27 | Description: acct.description, 28 | AcmeServer: acct.accountServerDb.toServer(), 29 | AccountKey: acct.accountKeyDb.toKey(), 30 | Status: acct.status, 31 | Email: acct.email, 32 | AcceptedTos: acct.acceptedTos, 33 | CreatedAt: acct.createdAt, 34 | UpdatedAt: acct.updatedAt, 35 | Kid: acct.kid, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/accounts_delete.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "certwarden-backend/pkg/storage" 5 | "context" 6 | ) 7 | 8 | // AccountHasCerts returns true if the specified accountId matches 9 | // any of the certificates in the db 10 | func (store *Storage) AccountHasCerts(accountId int) bool { 11 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 12 | defer cancel() 13 | 14 | // don't check account exists, business logic in app should do this 15 | 16 | // check account id is not in use in certificates 17 | query := ` 18 | SELECT id 19 | FROM certificates 20 | WHERE acme_account_id = $1 21 | ` 22 | 23 | row := store.db.QueryRowContext(ctx, query, accountId) 24 | temp := -2 25 | 26 | err := row.Scan(&temp) 27 | // error means no certs for the account (includes error no rows) 28 | return err == nil 29 | } 30 | 31 | // DeleteAccount deletes an account from the database 32 | func (store *Storage) DeleteAccount(id int) error { 33 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 34 | defer cancel() 35 | 36 | tx, err := store.db.BeginTx(ctx, nil) 37 | if err != nil { 38 | return err 39 | } 40 | defer tx.Rollback() 41 | 42 | // check acct exists 43 | // if scan in succeeds, key exists 44 | query := ` 45 | SELECT id 46 | FROM acme_accounts 47 | WHERE id = $1 48 | ` 49 | 50 | row := tx.QueryRowContext(ctx, query, id) 51 | temp := -2 52 | row.Scan(&temp) 53 | if temp == -2 { 54 | return storage.ErrNoRecord 55 | } 56 | 57 | // check not in use in certs 58 | // if scan in succeeds, record exists in certificates 59 | query = ` 60 | SELECT id 61 | FROM certificates 62 | WHERE acme_account_id = $1 63 | ` 64 | 65 | row = tx.QueryRowContext(ctx, query, id) 66 | temp = -2 67 | row.Scan(&temp) 68 | if temp != -2 { 69 | return storage.ErrInUse 70 | } 71 | 72 | // delete 73 | query = ` 74 | DELETE FROM 75 | acme_accounts 76 | WHERE 77 | id = $1 78 | ` 79 | 80 | _, err = tx.ExecContext(ctx, query, id) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | err = tx.Commit() 86 | if err != nil { 87 | return err 88 | } 89 | 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/accounts_post.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "certwarden-backend/pkg/domain/acme_accounts" 5 | "context" 6 | ) 7 | 8 | // PostNewAccount inserts a new account into the db 9 | func (store *Storage) PostNewAccount(payload acme_accounts.NewPayload) (acme_accounts.Account, error) { 10 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 11 | defer cancel() 12 | 13 | // don't check for in use in storage. main app business logic should 14 | // take care of it 15 | 16 | // insert the new account 17 | query := ` 18 | INSERT INTO acme_accounts (name, description, acme_server_id, private_key_id, status, email, 19 | accepted_tos, created_at, updated_at, kid) 20 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) 21 | RETURNING id 22 | ` 23 | 24 | id := -1 25 | err := store.db.QueryRowContext(ctx, query, 26 | payload.Name, 27 | payload.Description, 28 | payload.AcmeServerID, 29 | payload.PrivateKeyID, 30 | payload.Status, 31 | payload.Email, 32 | payload.AcceptedTos, 33 | payload.CreatedAt, 34 | payload.UpdatedAt, 35 | payload.Kid, 36 | ).Scan(&id) 37 | 38 | if err != nil { 39 | return acme_accounts.Account{}, err 40 | } 41 | 42 | // get new account to return 43 | newAccount, err := store.GetOneAccountById(id) 44 | if err != nil { 45 | return acme_accounts.Account{}, err 46 | } 47 | 48 | return newAccount, nil 49 | } 50 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/acme_servers.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import "certwarden-backend/pkg/domain/acme_servers" 4 | 5 | // acmeServerDb is a single acme server, as database table fields 6 | // corresponds to acme_servers.Server 7 | type acmeServerDb struct { 8 | id int 9 | name string 10 | description string 11 | directoryUrl string 12 | isStaging bool 13 | createdAt int 14 | updatedAt int 15 | } 16 | 17 | // toServer maps the database acme server info to the acme_servers 18 | // Server object 19 | func (serv acmeServerDb) toServer() acme_servers.Server { 20 | return acme_servers.Server{ 21 | ID: serv.id, 22 | Name: serv.name, 23 | Description: serv.description, 24 | DirectoryURL: serv.directoryUrl, 25 | IsStaging: serv.isStaging, 26 | CreatedAt: serv.createdAt, 27 | UpdatedAt: serv.updatedAt, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/acme_servers_delete.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "certwarden-backend/pkg/storage" 5 | "context" 6 | ) 7 | 8 | // ServerHasAccounts returns true if the specified serverId matches 9 | // any of the accounts in the db 10 | func (store *Storage) ServerHasAccounts(serverId int) bool { 11 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 12 | defer cancel() 13 | 14 | // check server id is not in use in certificates 15 | query := ` 16 | SELECT id 17 | FROM acme_accounts 18 | WHERE acme_server_id = $1 19 | ` 20 | 21 | row := store.db.QueryRowContext(ctx, query, serverId) 22 | temp := -2 23 | 24 | err := row.Scan(&temp) 25 | // error means no accounts for the server (includes error no rows) 26 | return err == nil 27 | } 28 | 29 | // DeleteServer deletes an acme server from the database 30 | func (store *Storage) DeleteServer(serverId int) error { 31 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 32 | defer cancel() 33 | 34 | tx, err := store.db.BeginTx(ctx, nil) 35 | if err != nil { 36 | return err 37 | } 38 | defer tx.Rollback() 39 | 40 | // check server exists 41 | // if scan in succeeds, key exists 42 | query := ` 43 | SELECT id 44 | FROM acme_servers 45 | WHERE id = $1 46 | ` 47 | 48 | row := tx.QueryRowContext(ctx, query, serverId) 49 | temp := -2 50 | row.Scan(&temp) 51 | if temp == -2 { 52 | return storage.ErrNoRecord 53 | } 54 | 55 | // check not in use in accounts 56 | // if scan in succeeds, record exists in accounts 57 | query = ` 58 | SELECT id 59 | FROM acme_accounts 60 | WHERE acme_server_id = $1 61 | ` 62 | 63 | row = tx.QueryRowContext(ctx, query, serverId) 64 | temp = -2 65 | row.Scan(&temp) 66 | if temp != -2 { 67 | return storage.ErrInUse 68 | } 69 | 70 | // delete 71 | query = ` 72 | DELETE FROM 73 | acme_servers 74 | WHERE 75 | id = $1 76 | ` 77 | 78 | _, err = tx.ExecContext(ctx, query, serverId) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | err = tx.Commit() 84 | if err != nil { 85 | return err 86 | } 87 | 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/acme_servers_post.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "certwarden-backend/pkg/domain/acme_servers" 5 | "context" 6 | ) 7 | 8 | // PostNewServer saves the KeyExtended to the db as a new key 9 | func (store *Storage) PostNewServer(payload acme_servers.NewPayload) (acme_servers.Server, error) { 10 | // database action 11 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 12 | defer cancel() 13 | 14 | query := ` 15 | INSERT INTO acme_servers (name, description, directory_url, is_staging, created_at, updated_at) 16 | VALUES ($1, $2, $3, $4, $5, $6) 17 | RETURNING id 18 | ` 19 | 20 | // insert and scan the new id 21 | acmeServerId := -1 22 | err := store.db.QueryRowContext(ctx, query, 23 | payload.Name, 24 | payload.Description, 25 | payload.DirectoryURL, 26 | payload.IsStaging, 27 | payload.CreatedAt, 28 | payload.UpdatedAt, 29 | ).Scan(&acmeServerId) 30 | 31 | if err != nil { 32 | return acme_servers.Server{}, err 33 | } 34 | 35 | // get updated server to return 36 | updatedServer, err := store.GetOneServerById(acmeServerId) 37 | if err != nil { 38 | return acme_servers.Server{}, err 39 | } 40 | 41 | return updatedServer, nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/acme_servers_put.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "certwarden-backend/pkg/domain/acme_servers" 5 | "context" 6 | ) 7 | 8 | // PutServerUpdate updates details about an acme Server 9 | func (store *Storage) PutServerUpdate(payload acme_servers.UpdatePayload) (acme_servers.Server, error) { 10 | // database update 11 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 12 | defer cancel() 13 | 14 | query := ` 15 | UPDATE 16 | acme_servers 17 | SET 18 | name = case when $1 is null then name else $1 end, 19 | description = case when $2 is null then description else $2 end, 20 | directory_url = case when $3 is null then directory_url else $3 end, 21 | is_staging = case when $4 is null then is_staging else $4 end, 22 | updated_at = $5 23 | WHERE 24 | id = $6 25 | ` 26 | 27 | _, err := store.db.ExecContext(ctx, query, 28 | payload.Name, 29 | payload.Description, 30 | payload.DirectoryURL, 31 | payload.IsStaging, 32 | payload.UpdatedAt, 33 | payload.ID, 34 | ) 35 | 36 | if err != nil { 37 | return acme_servers.Server{}, err 38 | } 39 | 40 | // get updated server to return 41 | updatedServer, err := store.GetOneServerById(payload.ID) 42 | if err != nil { 43 | return acme_servers.Server{}, err 44 | } 45 | 46 | return updatedServer, nil 47 | } 48 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/backup.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import "context" 4 | 5 | // LockForBackup starts a sql transaction that aquires a SHARED (read only) 6 | // lock on the db and returns a function to end that lock. File copy or other 7 | // filesystem backup action should be performed between the two functions 8 | func (store *Storage) LockDBForBackup() (unlockFunc func(), err error) { 9 | // start sql transaction 10 | // Do not use timeout, use background to ensure backup actions have all the 11 | // time they want to do the backup 12 | tx, err := store.db.BeginTx(context.Background(), nil) 13 | if err != nil { 14 | return nil, err 15 | } 16 | // no defer rollback, only call via unlockFunc 17 | 18 | // arbitrary select which will cause the SHARED lock to begin 19 | query := ` 20 | SELECT '' from acme_accounts 21 | ` 22 | 23 | _, err = tx.Exec(query) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | // make function to rollback the tx (remove the lock) 29 | unlockFunc = func() { 30 | _ = tx.Rollback() 31 | } 32 | 33 | return unlockFunc, nil 34 | } 35 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/certificates_delete.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "certwarden-backend/pkg/storage" 5 | "context" 6 | ) 7 | 8 | // DeleteCert deletes a cert from the database 9 | func (store *Storage) DeleteCert(id int) (err error) { 10 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 11 | defer cancel() 12 | 13 | tx, err := store.db.BeginTx(ctx, nil) 14 | if err != nil { 15 | return err 16 | } 17 | defer tx.Rollback() 18 | 19 | // check cert exists 20 | // if scan in succeeds, cert exists 21 | query := ` 22 | SELECT id 23 | FROM certificates 24 | WHERE id = $1 25 | ` 26 | 27 | row := tx.QueryRowContext(ctx, query, id) 28 | temp := -2 29 | row.Scan(&temp) 30 | if temp == -2 { 31 | return storage.ErrNoRecord 32 | } 33 | 34 | // delete 35 | query = ` 36 | DELETE FROM 37 | certificates 38 | WHERE 39 | id = $1 40 | ` 41 | 42 | _, err = tx.ExecContext(ctx, query, id) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | err = tx.Commit() 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/certificates_post.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "certwarden-backend/pkg/domain/certificates" 5 | "context" 6 | ) 7 | 8 | // PostNewAccount inserts a new cert into the db 9 | func (store *Storage) PostNewCert(payload certificates.NewPayload) (certificates.Certificate, error) { 10 | // database update 11 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 12 | defer cancel() 13 | 14 | // don't check for in use in storage. main app business logic should 15 | // take care of it 16 | 17 | // insert the new cert 18 | query := ` 19 | INSERT INTO certificates (name, description, private_key_id, acme_account_id, subject, subject_alts, 20 | csr_org, csr_ou, csr_country, csr_state, csr_city, csr_extra_extensions, preferred_root_cn, 21 | created_at, updated_at, api_key, api_key_via_url, 22 | post_processing_command, post_processing_environment, post_processing_client_address, 23 | post_processing_client_key, profile) 24 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22) 25 | RETURNING id 26 | ` 27 | 28 | id := -1 29 | err := store.db.QueryRowContext(ctx, query, 30 | payload.Name, 31 | payload.Description, 32 | payload.PrivateKeyID, 33 | payload.AcmeAccountID, 34 | payload.Subject, 35 | makeJsonStringSlice(payload.SubjectAltNames), 36 | payload.Organization, 37 | payload.OrganizationalUnit, 38 | payload.Country, 39 | payload.State, 40 | payload.City, 41 | makeJsonCertExtensionSlice(payload.CSRExtraExtensions), 42 | payload.PreferredRootCN, 43 | payload.CreatedAt, 44 | payload.UpdatedAt, 45 | payload.ApiKey, 46 | payload.ApiKeyViaUrl, 47 | payload.PostProcessingCommand, 48 | makeJsonStringSlice(payload.PostProcessingEnvironment), 49 | payload.PostProcessingClientKeyB64, 50 | payload.PostProcessingClientAddress, 51 | payload.Profile, 52 | ).Scan(&id) 53 | 54 | if err != nil { 55 | return certificates.Certificate{}, err 56 | } 57 | 58 | // get updated to return 59 | newCert, err := store.GetOneCertById(id) 60 | if err != nil { 61 | return certificates.Certificate{}, err 62 | } 63 | 64 | return newCert, nil 65 | } 66 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/keys.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "certwarden-backend/pkg/domain/private_keys" 5 | "certwarden-backend/pkg/domain/private_keys/key_crypto" 6 | "time" 7 | ) 8 | 9 | // keyDb is a single private key, as database table fields 10 | // corresponds to private_keys.Key 11 | type keyDb struct { 12 | id int 13 | name string 14 | description string 15 | algorithmValue string 16 | pem string 17 | apiKey string 18 | apiKeyNew string 19 | apiKeyDisabled bool 20 | apiKeyViaUrl bool 21 | lastAccess int64 22 | createdAt int64 23 | updatedAt int64 24 | } 25 | 26 | // toKey maps the database key info to the private_keys Key 27 | // object 28 | func (key keyDb) toKey() private_keys.Key { 29 | return private_keys.Key{ 30 | ID: key.id, 31 | Name: key.name, 32 | Description: key.description, 33 | Algorithm: key_crypto.AlgorithmByStorageValue(key.algorithmValue), 34 | Pem: key.pem, 35 | ApiKey: key.apiKey, 36 | ApiKeyNew: key.apiKeyNew, 37 | ApiKeyDisabled: key.apiKeyDisabled, 38 | ApiKeyViaUrl: key.apiKeyViaUrl, 39 | LastAccess: time.Unix(key.lastAccess, 0), 40 | CreatedAt: time.Unix(key.createdAt, 0), 41 | UpdatedAt: time.Unix(key.updatedAt, 0), 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/keys_post.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "certwarden-backend/pkg/domain/private_keys" 5 | "context" 6 | ) 7 | 8 | // PostNewKey saves the KeyExtended to the db as a new key 9 | func (store *Storage) PostNewKey(payload private_keys.NewPayload) (private_keys.Key, error) { 10 | // database action 11 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 12 | defer cancel() 13 | 14 | query := ` 15 | INSERT INTO private_keys (name, description, algorithm, pem, api_key, api_key_disabled, api_key_via_url, created_at, updated_at) 16 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) 17 | RETURNING id 18 | ` 19 | 20 | // insert and scan the new id 21 | id := -1 22 | err := store.db.QueryRowContext(ctx, query, 23 | payload.Name, 24 | payload.Description, 25 | payload.AlgorithmValue, 26 | payload.PemContent, 27 | payload.ApiKey, 28 | payload.ApiKeyDisabled, 29 | payload.ApiKeyViaUrl, 30 | payload.CreatedAt, 31 | payload.UpdatedAt, 32 | ).Scan(&id) 33 | 34 | if err != nil { 35 | return private_keys.Key{}, err 36 | } 37 | 38 | // get updated key to return 39 | updatedKey, err := store.GetOneKeyById(id) 40 | if err != nil { 41 | return private_keys.Key{}, err 42 | } 43 | 44 | return updatedKey, nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/nulltypes.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | ) 7 | 8 | // Funcs to transform sql types into correspoinding pointer type 9 | 10 | // nullInt32UnixToTime converts a NullInt32 into an a time.Time pointer 11 | func nullInt32UnixToTime(nullInt sql.NullInt32) *time.Time { 12 | if nullInt.Valid { 13 | t := time.Unix(int64(nullInt.Int32), 0) 14 | 15 | return &t 16 | } 17 | 18 | return nil 19 | } 20 | 21 | // NullInt32ToInt converts a NullInt32 into an int pointer 22 | func nullInt32ToInt(nullInt sql.NullInt32) *int { 23 | if nullInt.Valid { 24 | i := new(int) 25 | *i = int(nullInt.Int32) 26 | 27 | return i 28 | } 29 | 30 | return nil 31 | } 32 | 33 | // nullStringToString converts the nullstring to a string pointer 34 | func nullStringToString(nullString sql.NullString) *string { 35 | if nullString.Valid { 36 | s := new(string) 37 | *s = nullString.String 38 | 39 | return s 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/orders_post.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "certwarden-backend/pkg/domain/orders" 5 | "context" 6 | "database/sql" 7 | ) 8 | 9 | // PostNewOrder makes a new order in the db. An error is returned if the order 10 | // location already exists (or any other error) 11 | func (store *Storage) PostNewOrder(payload orders.NewOrderAcmePayload) (newId int, err error) { 12 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 13 | defer cancel() 14 | 15 | // transaction 16 | tx, err := store.db.BeginTx(ctx, nil) 17 | if err != nil { 18 | return -2, err 19 | } 20 | defer tx.Rollback() 21 | 22 | // check if the order already exists 23 | query := ` 24 | SELECT 25 | id 26 | FROM 27 | acme_orders 28 | WHERE 29 | acme_location = $1 30 | ` 31 | 32 | row := tx.QueryRowContext(ctx, query, payload.Location) 33 | err = row.Scan(&newId) 34 | 35 | // if err == nil, record was found. return the existingId and a corresponding error 36 | if err == nil { 37 | return newId, orders.ErrOrderExists 38 | } else if err != sql.ErrNoRows { 39 | // any other error, except no rows (because that indicates this order is truly new to db) 40 | return -2, err 41 | } 42 | 43 | query = ` 44 | INSERT INTO 45 | acme_orders 46 | ( 47 | certificate_id, 48 | acme_account_id, 49 | status, 50 | known_revoked, 51 | expires, 52 | dns_identifiers, 53 | error, 54 | authorizations, 55 | finalize, 56 | profile, 57 | acme_location, 58 | created_at, 59 | updated_at 60 | ) 61 | VALUES 62 | ( 63 | $1, 64 | $2, 65 | $3, 66 | $4, 67 | $5, 68 | $6, 69 | $7, 70 | $8, 71 | $9, 72 | $10, 73 | $11, 74 | $12, 75 | $13 76 | ) 77 | RETURNING 78 | id 79 | ` 80 | 81 | err = tx.QueryRowContext(ctx, query, 82 | payload.CertId, 83 | payload.AccountId, 84 | payload.Status, 85 | payload.KnownRevoked, 86 | payload.Expires, 87 | makeJsonStringSlice(payload.DnsIds), 88 | payload.Error, 89 | makeJsonStringSlice(payload.Authorizations), 90 | payload.Finalize, 91 | payload.Profile, 92 | payload.Location, 93 | payload.CreatedAt, 94 | payload.UpdatedAt, 95 | ).Scan(&newId) 96 | 97 | err = tx.Commit() 98 | if err != nil { 99 | return -2, err 100 | } 101 | 102 | // TODO: Handle 0 rows updated. 103 | 104 | return newId, nil 105 | } 106 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/setup_migrate_v4.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // CHANGES v3 to v4: 9 | // - certificates: 10 | // - Add 'post_processing_client_key' field/column 11 | 12 | // migrateV3toV4 updates the storage db from user_version 3 to user_version 4, if it cannot 13 | // do so, an error is returned and modification is aborted 14 | func (store *Storage) migrateV3toV4() (int, error) { 15 | oldSchemaVer := 3 16 | newSchemaVer := 4 17 | 18 | store.logger.Infof("updating database user_version from %d to %d", oldSchemaVer, newSchemaVer) 19 | 20 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 21 | defer cancel() 22 | 23 | // create sql transaction to roll back in the event an error occurs 24 | tx, err := store.db.BeginTx(ctx, nil) 25 | if err != nil { 26 | return -1, err 27 | } 28 | defer tx.Rollback() 29 | 30 | // verify correct current ver 31 | query := `PRAGMA user_version` 32 | row := tx.QueryRowContext(ctx, query) 33 | fileUserVersion := -1 34 | err = row.Scan( 35 | &fileUserVersion, 36 | ) 37 | if err != nil { 38 | return -1, err 39 | } 40 | if fileUserVersion != oldSchemaVer { 41 | return -1, fmt.Errorf("cannot update db schema, current version %d (expected %d)", fileUserVersion, oldSchemaVer) 42 | } 43 | 44 | // add columns 45 | query = ` 46 | ALTER TABLE certificates ADD post_processing_client_key text NOT NULL DEFAULT ""; 47 | ` 48 | 49 | _, err = tx.Exec(query) 50 | if err != nil { 51 | return -1, err 52 | } 53 | 54 | // update user_version 55 | query = fmt.Sprintf(` 56 | PRAGMA user_version = %d 57 | `, newSchemaVer) 58 | 59 | _, err = tx.Exec(query) 60 | if err != nil { 61 | return -1, err 62 | } 63 | 64 | // no errors, commit transaction 65 | err = tx.Commit() 66 | if err != nil { 67 | return -1, err 68 | } 69 | 70 | store.logger.Infof("database user_version successfully upgraded from %d to %d", oldSchemaVer, newSchemaVer) 71 | return newSchemaVer, nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/setup_migrate_v5.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // CHANGES v4 to v5: 9 | // - certificates: 10 | // - Add 'post_processing_client_key' field/column 11 | 12 | // migrateV4toV5 updates the storage db from user_version 4 to user_version 5, if it cannot 13 | // do so, an error is returned and modification is aborted 14 | func (store *Storage) migrateV4toV5() (int, error) { 15 | oldSchemaVer := 4 16 | newSchemaVer := 5 17 | 18 | store.logger.Infof("updating database user_version from %d to %d", oldSchemaVer, newSchemaVer) 19 | 20 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 21 | defer cancel() 22 | 23 | // create sql transaction to roll back in the event an error occurs 24 | tx, err := store.db.BeginTx(ctx, nil) 25 | if err != nil { 26 | return -1, err 27 | } 28 | defer tx.Rollback() 29 | 30 | // verify correct current ver 31 | query := `PRAGMA user_version` 32 | row := tx.QueryRowContext(ctx, query) 33 | fileUserVersion := -1 34 | err = row.Scan( 35 | &fileUserVersion, 36 | ) 37 | if err != nil { 38 | return -1, err 39 | } 40 | if fileUserVersion != oldSchemaVer { 41 | return -1, fmt.Errorf("cannot update db schema, current version %d (expected %d)", fileUserVersion, oldSchemaVer) 42 | } 43 | 44 | // add columns 45 | query = ` 46 | ALTER TABLE certificates ADD csr_extra_extensions text NOT NULL DEFAULT "[]"; 47 | ` 48 | 49 | _, err = tx.Exec(query) 50 | if err != nil { 51 | return -1, err 52 | } 53 | 54 | // update user_version 55 | query = fmt.Sprintf(` 56 | PRAGMA user_version = %d 57 | `, newSchemaVer) 58 | 59 | _, err = tx.Exec(query) 60 | if err != nil { 61 | return -1, err 62 | } 63 | 64 | // no errors, commit transaction 65 | err = tx.Commit() 66 | if err != nil { 67 | return -1, err 68 | } 69 | 70 | store.logger.Infof("database user_version successfully upgraded from %d to %d", oldSchemaVer, newSchemaVer) 71 | return newSchemaVer, nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/setup_migrate_v6.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // CHANGES v5 to v6: 9 | // - certificates: 10 | // - If a certificate with the name `legocerthub` exists, rename it to `serverdefault` 11 | // Note: DB Schema doesn't actually change from 5 to 6. 12 | 13 | // migrateV5toV6 updates the storage db from user_version 5 to user_version 6, if it cannot 14 | // do so, an error is returned and modification is aborted 15 | func (store *Storage) migrateV5toV6() (int, error) { 16 | oldSchemaVer := 5 17 | newSchemaVer := 6 18 | 19 | store.logger.Infof("updating database user_version from %d to %d", oldSchemaVer, newSchemaVer) 20 | 21 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 22 | defer cancel() 23 | 24 | // create sql transaction to roll back in the event an error occurs 25 | tx, err := store.db.BeginTx(ctx, nil) 26 | if err != nil { 27 | return -1, err 28 | } 29 | defer tx.Rollback() 30 | 31 | // verify correct current ver 32 | query := `PRAGMA user_version` 33 | row := tx.QueryRowContext(ctx, query) 34 | fileUserVersion := -1 35 | err = row.Scan( 36 | &fileUserVersion, 37 | ) 38 | if err != nil { 39 | return -1, err 40 | } 41 | if fileUserVersion != oldSchemaVer { 42 | return -1, fmt.Errorf("cannot update db schema, current version %d (expected %d)", fileUserVersion, oldSchemaVer) 43 | } 44 | 45 | // if it exists, rename certificate `legocerthub` to `serverdefault` 46 | query = ` 47 | UPDATE certificates 48 | SET name = 'serverdefault' 49 | WHERE name = 'legocerthub' 50 | ` 51 | _, err = tx.ExecContext(ctx, query) 52 | if err != nil { 53 | return -1, err 54 | } 55 | 56 | // update user_version 57 | query = fmt.Sprintf(` 58 | PRAGMA user_version = %d 59 | `, newSchemaVer) 60 | 61 | _, err = tx.Exec(query) 62 | if err != nil { 63 | return -1, err 64 | } 65 | 66 | // no errors, commit transaction 67 | err = tx.Commit() 68 | if err != nil { 69 | return -1, err 70 | } 71 | 72 | store.logger.Infof("database user_version successfully upgraded from %d to %d", oldSchemaVer, newSchemaVer) 73 | return newSchemaVer, nil 74 | } 75 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/setup_migrate_v7.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // CHANGES v6 to v7: 9 | // - certificates: 10 | // - Add 'preferred_root_cn' field/column 11 | // - acme_orders: 12 | // - Add 'chain_root_cn' field/column 13 | 14 | // migrateV6toV7 updates the storage db from user_version 6 to user_version 7, if it cannot 15 | // do so, an error is returned and modification is aborted 16 | func (store *Storage) migrateV6toV7() (int, error) { 17 | oldSchemaVer := 6 18 | newSchemaVer := 7 19 | 20 | store.logger.Infof("updating database user_version from %d to %d", oldSchemaVer, newSchemaVer) 21 | 22 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 23 | defer cancel() 24 | 25 | // create sql transaction to roll back in the event an error occurs 26 | tx, err := store.db.BeginTx(ctx, nil) 27 | if err != nil { 28 | return -1, err 29 | } 30 | defer tx.Rollback() 31 | 32 | // verify correct current ver 33 | query := `PRAGMA user_version` 34 | row := tx.QueryRowContext(ctx, query) 35 | fileUserVersion := -1 36 | err = row.Scan( 37 | &fileUserVersion, 38 | ) 39 | if err != nil { 40 | return -1, err 41 | } 42 | if fileUserVersion != oldSchemaVer { 43 | return -1, fmt.Errorf("cannot update db schema, current version %d (expected %d)", fileUserVersion, oldSchemaVer) 44 | } 45 | 46 | // add columns 47 | query = ` 48 | ALTER TABLE certificates ADD preferred_root_cn text NOT NULL DEFAULT ""; 49 | ` 50 | 51 | _, err = tx.Exec(query) 52 | if err != nil { 53 | return -1, err 54 | } 55 | 56 | query = ` 57 | ALTER TABLE acme_orders ADD chain_root_cn text; 58 | ` 59 | 60 | _, err = tx.Exec(query) 61 | if err != nil { 62 | return -1, err 63 | } 64 | 65 | // update user_version 66 | query = fmt.Sprintf(` 67 | PRAGMA user_version = %d 68 | `, newSchemaVer) 69 | 70 | _, err = tx.Exec(query) 71 | if err != nil { 72 | return -1, err 73 | } 74 | 75 | // no errors, commit transaction 76 | err = tx.Commit() 77 | if err != nil { 78 | return -1, err 79 | } 80 | 81 | store.logger.Infof("database user_version successfully upgraded from %d to %d", oldSchemaVer, newSchemaVer) 82 | return newSchemaVer, nil 83 | } 84 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/setup_migrate_v8.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // CHANGES v7 to v8: 9 | // - certificates: 10 | // - Add 'last_access' field/column 11 | // - private_keys: 12 | // - Add 'last_access' field/column 13 | 14 | // migrateV7toV8 updates the storage db from user_version 7 to user_version 8, if it cannot 15 | // do so, an error is returned and modification is aborted 16 | func (store *Storage) migrateV7toV8() (int, error) { 17 | oldSchemaVer := 7 18 | newSchemaVer := 8 19 | 20 | store.logger.Infof("updating database user_version from %d to %d", oldSchemaVer, newSchemaVer) 21 | 22 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 23 | defer cancel() 24 | 25 | // create sql transaction to roll back in the event an error occurs 26 | tx, err := store.db.BeginTx(ctx, nil) 27 | if err != nil { 28 | return -1, err 29 | } 30 | defer tx.Rollback() 31 | 32 | // verify correct current ver 33 | query := `PRAGMA user_version` 34 | row := tx.QueryRowContext(ctx, query) 35 | fileUserVersion := -1 36 | err = row.Scan( 37 | &fileUserVersion, 38 | ) 39 | if err != nil { 40 | return -1, err 41 | } 42 | if fileUserVersion != oldSchemaVer { 43 | return -1, fmt.Errorf("cannot update db schema, current version %d (expected %d)", fileUserVersion, oldSchemaVer) 44 | } 45 | 46 | // add columns 47 | query = ` 48 | ALTER TABLE certificates ADD last_access integer NOT NULL DEFAULT 0; 49 | ` 50 | 51 | _, err = tx.Exec(query) 52 | if err != nil { 53 | return -1, err 54 | } 55 | 56 | query = ` 57 | ALTER TABLE private_keys ADD last_access integer NOT NULL DEFAULT 0; 58 | ` 59 | 60 | _, err = tx.Exec(query) 61 | if err != nil { 62 | return -1, err 63 | } 64 | 65 | // update user_version 66 | query = fmt.Sprintf(` 67 | PRAGMA user_version = %d 68 | `, newSchemaVer) 69 | 70 | _, err = tx.Exec(query) 71 | if err != nil { 72 | return -1, err 73 | } 74 | 75 | // no errors, commit transaction 76 | err = tx.Commit() 77 | if err != nil { 78 | return -1, err 79 | } 80 | 81 | store.logger.Infof("database user_version successfully upgraded from %d to %d", oldSchemaVer, newSchemaVer) 82 | return newSchemaVer, nil 83 | } 84 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/setup_migrate_v9.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // CHANGES v8 to v9: 9 | // - certificates: 10 | // - Add 'post_processing_client_address' field/column 11 | 12 | // migrateV8toV9 updates the storage db from user_version 8 to user_version 9, if it cannot 13 | // do so, an error is returned and modification is aborted 14 | func (store *Storage) migrateV8toV9() (int, error) { 15 | oldSchemaVer := 8 16 | newSchemaVer := 9 17 | 18 | store.logger.Infof("updating database user_version from %d to %d", oldSchemaVer, newSchemaVer) 19 | 20 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 21 | defer cancel() 22 | 23 | // create sql transaction to roll back in the event an error occurs 24 | tx, err := store.db.BeginTx(ctx, nil) 25 | if err != nil { 26 | return -1, err 27 | } 28 | defer tx.Rollback() 29 | 30 | // verify correct current ver 31 | query := `PRAGMA user_version` 32 | row := tx.QueryRowContext(ctx, query) 33 | fileUserVersion := -1 34 | err = row.Scan( 35 | &fileUserVersion, 36 | ) 37 | if err != nil { 38 | return -1, err 39 | } 40 | if fileUserVersion != oldSchemaVer { 41 | return -1, fmt.Errorf("cannot update db schema, current version %d (expected %d)", fileUserVersion, oldSchemaVer) 42 | } 43 | 44 | // add column 45 | query = ` 46 | ALTER TABLE certificates ADD post_processing_client_address text NOT NULL DEFAULT ""; 47 | ` 48 | 49 | _, err = tx.Exec(query) 50 | if err != nil { 51 | return -1, err 52 | } 53 | 54 | // populate `post_processing_client_address` if `post_processing_client_key` is not empty 55 | query = ` 56 | UPDATE certificates 57 | SET post_processing_client_address = subject 58 | WHERE post_processing_client_key <> '' 59 | ` 60 | _, err = tx.ExecContext(ctx, query) 61 | if err != nil { 62 | return -1, err 63 | } 64 | 65 | // update user_version 66 | query = fmt.Sprintf(` 67 | PRAGMA user_version = %d 68 | `, newSchemaVer) 69 | 70 | _, err = tx.Exec(query) 71 | if err != nil { 72 | return -1, err 73 | } 74 | 75 | // no errors, commit transaction 76 | err = tx.Commit() 77 | if err != nil { 78 | return -1, err 79 | } 80 | 81 | store.logger.Infof("database user_version successfully upgraded from %d to %d", oldSchemaVer, newSchemaVer) 82 | return newSchemaVer, nil 83 | } 84 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/time.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // timeNow() returns unix time as a NullInt32 8 | func timeNow() (unixTime int) { 9 | return int(time.Now().Unix()) 10 | } 11 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/types.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "certwarden-backend/pkg/domain/certificates" 5 | "encoding/json" 6 | ) 7 | 8 | // jsonStringSlice is a string type in storage that is a json formatted 9 | // array of strings 10 | type jsonStringSlice string 11 | 12 | // transform JSS into string slice 13 | func (jss jsonStringSlice) toSlice() []string { 14 | if jss == "" { 15 | return []string{} 16 | } 17 | 18 | strSlice := []string{} 19 | err := json.Unmarshal([]byte(jss), &strSlice) 20 | if err != nil { 21 | return []string{} 22 | } 23 | 24 | return strSlice 25 | } 26 | 27 | // makeCommaJoinedString creates a JSS from a slice of strings 28 | func makeJsonStringSlice(stringSlice []string) jsonStringSlice { 29 | if len(stringSlice) == 0 { 30 | return "[]" 31 | } 32 | 33 | jss, err := json.Marshal(stringSlice) 34 | if err != nil { 35 | return "[]" 36 | } 37 | 38 | return jsonStringSlice(jss) 39 | } 40 | 41 | // jsonCertExtensionSlice is a json formatted string that is a slice of CertExtension 42 | type jsonCertExtensionSlice string 43 | 44 | // transform JCES into a slice of proper CertExtension 45 | func (jces jsonCertExtensionSlice) toCertExtensionSlice() ([]certificates.CertExtension, error) { 46 | if jces == "" { 47 | return []certificates.CertExtension{}, nil 48 | } 49 | 50 | // unmarshal the json to the json object 51 | extSlice := []certificates.CertExtensionJSON{} 52 | err := json.Unmarshal([]byte(jces), &extSlice) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | // convert json objs to real objs 58 | certExtSlice := []certificates.CertExtension{} 59 | for i := range extSlice { 60 | certExt, err := extSlice[i].ToCertExtension() 61 | if err != nil { 62 | // if invalid data stored, return err 63 | return nil, err 64 | } 65 | certExtSlice = append(certExtSlice, certExt) 66 | } 67 | 68 | return certExtSlice, nil 69 | } 70 | 71 | // makeJsonCertExtensionSlice creates a JCES from a slice of CertExtensionJSON 72 | func makeJsonCertExtensionSlice(extensionSlice []certificates.CertExtensionJSON) jsonCertExtensionSlice { 73 | if len(extensionSlice) == 0 { 74 | return "[]" 75 | } 76 | 77 | jpes, err := json.Marshal(extensionSlice) 78 | if err != nil { 79 | return "[]" 80 | } 81 | 82 | return jsonCertExtensionSlice(jpes) 83 | } 84 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/users.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | // userDb represents how users are stored in the db 4 | type userDb struct { 5 | id int 6 | username string 7 | passwordHash string 8 | createdAt int 9 | updatedAt int 10 | } 11 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/users_get.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "certwarden-backend/pkg/domain/app/auth" 5 | "certwarden-backend/pkg/storage" 6 | "context" 7 | "database/sql" 8 | "errors" 9 | ) 10 | 11 | // dbToUser converts the user db object to app object 12 | func (userDb *userDb) dbToUser() (user auth.User) { 13 | return auth.User{ 14 | ID: userDb.id, 15 | Username: userDb.username, 16 | PasswordHash: userDb.passwordHash, 17 | CreatedAt: userDb.createdAt, 18 | UpdatedAt: userDb.updatedAt, 19 | } 20 | } 21 | 22 | // GetOneUserByName returns a user from the db based on 23 | // username 24 | func (store Storage) GetOneUserByName(username string) (auth.User, error) { 25 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 26 | defer cancel() 27 | 28 | query := ` 29 | SELECT 30 | id, username, password_hash, created_at, updated_at 31 | FROM 32 | users 33 | WHERE 34 | username = $1 35 | ` 36 | 37 | row := store.db.QueryRowContext(ctx, query, username) 38 | 39 | var user userDb 40 | err := row.Scan( 41 | &user.id, 42 | &user.username, 43 | &user.passwordHash, 44 | &user.createdAt, 45 | &user.updatedAt, 46 | ) 47 | 48 | if err != nil { 49 | // if no record exists 50 | if errors.Is(err, sql.ErrNoRows) { 51 | err = storage.ErrNoRecord 52 | } 53 | return auth.User{}, err 54 | } 55 | 56 | convertedUser := user.dbToUser() 57 | 58 | return convertedUser, nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/users_put.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import "context" 4 | 5 | // UpdateUserPassword updates the specified user's password hash to the specified 6 | // hash. 7 | func (store *Storage) UpdateUserPassword(username string, newPasswordHash string) (userId int, err error) { 8 | // database action 9 | ctx, cancel := context.WithTimeout(context.Background(), store.timeout) 10 | defer cancel() 11 | 12 | query := ` 13 | UPDATE 14 | users 15 | SET 16 | password_hash = $1, 17 | updated_at = $2 18 | WHERE 19 | username = $3 20 | RETURNING 21 | id 22 | ` 23 | 24 | // update password and return id 25 | err = store.db.QueryRowContext(ctx, query, 26 | newPasswordHash, 27 | timeNow(), 28 | username, 29 | ).Scan(&userId) 30 | 31 | if err != nil { 32 | return -2, err 33 | } 34 | 35 | return userId, nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/validation/domain.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | const DomainValidRegex = `^(([A-Za-z0-9][A-Za-z0-9-]{0,61}\.)*([A-Za-z0-9][A-Za-z0-9-]{0,61}\.)[A-Za-z][A-Za-z0-9-]{0,61}[A-Za-z0-9])$` 9 | const URLValidRegex = `^[A-Za-z0-9-_.~!#$&'()*+,/:;=?@%[\]]*$` 10 | 11 | // DomainValid returns true if the string is a validly formatted 12 | // domain name 13 | // https://tools.ietf.org/id/draft-liman-tld-names-00.html 14 | // this is likely more inclusive than ACME server will permit 15 | // TODO(?): restrict this further 16 | func DomainValid(domain string, wildOk bool) bool { 17 | // if wildcard is allowed (for certs it is allowed per RFC 8555 7.1.3) 18 | if wildOk { 19 | // if string prefix is wildcard ("*."), remove it and then validate the remainder 20 | // if the prefix is not *. this call is a no-op 21 | domain = strings.TrimPrefix(domain, "*.") 22 | } 23 | 24 | return regexp.MustCompile(DomainValidRegex).MatchString(domain) 25 | } 26 | 27 | // HttpsUrlValid returns true if the string contains only valid URL characters 28 | func HttpsUrlValid(url string) bool { 29 | if !strings.HasPrefix(url, "https://") { 30 | return false 31 | } 32 | 33 | return regexp.MustCompile(URLValidRegex).MatchString(url) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/validation/email.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | // EmailUsernameRegex is the regex to confirm an email username is in the proper 9 | // form. 10 | const emailUsernameRegex = `^[A-Za-z0-9][A-Za-z0-9-_.+]{0,62}[A-Za-z0-9]$` 11 | 12 | // invalidConsecutiveSpecialRegex matches if the username contains two special 13 | // chars in a row, which means it is not valid 14 | const invalidConsecutiveSpecialRegex = `[-_.+]{2,}` 15 | 16 | // EmailValid returns true if the string contains a validly formatted email address 17 | func EmailValid(email string) bool { 18 | // split on @ and validate username and domain 19 | // also confirms exactly 1 @ symbol 20 | emailPieces := strings.Split(email, "@") 21 | if len(emailPieces) != 2 { 22 | return false 23 | } 24 | username := emailPieces[0] 25 | domain := emailPieces[1] 26 | 27 | // validate username 28 | if !regexp.MustCompile(emailUsernameRegex).MatchString(username) || 29 | regexp.MustCompile(invalidConsecutiveSpecialRegex).MatchString(username) { 30 | return false 31 | } 32 | 33 | // validate domain 34 | if !DomainValid(domain, false) { 35 | return false 36 | } 37 | 38 | return true 39 | } 40 | 41 | // EmailValidOrBlank returns true if the email is blank or 42 | // contains a valid email format 43 | func EmailValidOrBlank(email string) bool { 44 | return email == "" || EmailValid(email) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/validation/id.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | const newId = -1 4 | 5 | // IsIdNew returns true if the id is the new id value 6 | func IsIdNew(id int) bool { 7 | return id == newId 8 | } 9 | 10 | // IsIdExistingValidRange returns true if the id is greater than or equal 11 | // to 0 and is not the newId. 12 | func IsIdExistingValidRange(id int) bool { 13 | return id != newId && id >= 0 14 | } 15 | -------------------------------------------------------------------------------- /pkg/validation/name.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | // NameValidRegex is the regex to confirm a name is in the proper 8 | // form. (Note: if match is found, name is INVALID) 9 | const NameValidRegex = `[^-_.~A-z0-9]|[\^]` 10 | 11 | // NameValid true if the specified name is acceptable. To be valid 12 | // the name must only contain symbols - _ . ~ letters and numbers, 13 | // and name cannot be blank (len <= 0) 14 | func NameValid(name string) bool { 15 | // length 16 | if len(name) <= 0 { 17 | return false 18 | } 19 | 20 | // validate (if this matches, it is INVALID) 21 | return !(regexp.MustCompile(NameValidRegex).Match([]byte(name))) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/validation/name_test.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import "testing" 4 | 5 | var validNames = []string{ 6 | "test", 7 | "aName", 8 | "sOmENaMEee", 9 | "name.com", 10 | "name.com.", 11 | "some.name.com...", 12 | "som~name.here", 13 | "myTest_-Name", 14 | } 15 | 16 | var invalidNames = []string{ 17 | "", 18 | " ", 19 | " ", 20 | "a Name", 21 | " aName", 22 | "aName ", 23 | "some$name", 24 | } 25 | 26 | func TestValidation_NameValid(t *testing.T) { 27 | // test valid names 28 | for _, name := range validNames { 29 | valid := NameValid(name) 30 | if !valid { 31 | t.Errorf("valid name test case '%s' returned invalid", name) 32 | } 33 | } 34 | 35 | // test invalid names 36 | for _, name := range invalidNames { 37 | valid := NameValid(name) 38 | if valid { 39 | t.Errorf("invalid name test case '%s' returned valid", name) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /scripts/linux/_warning_store_your_scripts_in_data_folder: -------------------------------------------------------------------------------- 1 | This folder is not backed up by the application. 2 | 3 | Store any customization somewhere in ./data/... 4 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/acme_sh_prep.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This script transforms the 'stock' acme.sh files into the expected format 4 | # for Cert Warden. This script should be run any time the acme.sh version 5 | # is bumped. 6 | 7 | # This shifts the script processing to once pre-release vs. repeatedly during 8 | # runtime. 9 | 10 | import os 11 | import shutil 12 | 13 | # keep in sync with `pkg\challenges\providers\dns01acmesh\cmd.go` 14 | dnsApiCwPath = "/dnsapi_cw" 15 | 16 | ### 17 | ### 18 | 19 | # script to add to dnsapi scripts 20 | source_script = """ 21 | ABS_CURR_PATH=$(dirname $(realpath "${BASH_SOURCE[0]}")) 22 | SRC_FILE="${ABS_CURR_PATH}/../acme_src.sh" 23 | . "${SRC_FILE}" 24 | 25 | """ 26 | 27 | # make dnsapi path relative 28 | dnsApiCwPath = "." + dnsApiCwPath 29 | 30 | # verify acme.sh is in the current path 31 | if not os.path.exists("acme.sh"): 32 | print("abort: acme.sh not found in current working directory") 33 | exit(-1) 34 | 35 | if not os.path.exists("dnsapi"): 36 | print("abort: acme.sh dnsapi path not found in current working directory") 37 | exit(-1) 38 | 39 | # delete any previously generated files 40 | if os.path.exists(dnsApiCwPath): 41 | shutil.rmtree(dnsApiCwPath) 42 | if os.path.exists("acme_src.sh"): 43 | os.remove("acme_src.sh") 44 | 45 | # read in main script 46 | acmeshData = "" 47 | with open('acme.sh') as f: 48 | acmeshData = f.read() 49 | 50 | # remove line that runs main -- `main "$@"` 51 | acmeshData = acmeshData.replace('main "$@"', "") 52 | 53 | # write acme_src.sh 54 | acmeshSrcF = open("acme_src.sh", "w") 55 | acmeshSrcF.write(acmeshData) 56 | 57 | # create cw folder if doesn't exist 58 | if not os.path.exists(dnsApiCwPath): 59 | os.makedirs(dnsApiCwPath) 60 | 61 | # process each dnsapi file 62 | for filename in os.listdir("dnsapi"): 63 | # only process scripts 64 | if not filename.endswith(".sh"): 65 | continue 66 | 67 | # read file in, preserve shebang, add source directive, and then the rest of the script 68 | dnsData = "" 69 | with open("dnsapi/" + filename) as f: 70 | # read in first line 71 | shebang = f.readline() 72 | 73 | # read the rest 74 | script = f.read() 75 | 76 | # combine 77 | dnsData = shebang + source_script + script 78 | 79 | # write to CW custom folder 80 | cwF = open(dnsApiCwPath + "/" + filename, "w") 81 | cwF.write(dnsData) 82 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/dnsapi/README.md: -------------------------------------------------------------------------------- 1 | # How to use DNS API 2 | DNS api usage: 3 | 4 | 5 | https://github.com/acmesh-official/acme.sh/wiki/dnsapi 6 | 7 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/dnsapi/dns_df.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC2034 3 | dns_df_info='DynDnsFree.de 4 | Domains: dynup.de 5 | Site: DynDnsFree.de 6 | Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_df 7 | Options: 8 | DF_user Username 9 | DF_password Password 10 | Issues: github.com/acmesh-official/acme.sh/issues/2897 11 | Author: Thilo Gass 12 | ' 13 | 14 | dyndnsfree_api="https://dynup.de/acme.php" 15 | 16 | dns_df_add() { 17 | fulldomain=$1 18 | txt_value=$2 19 | _info "Using DNS-01 dyndnsfree.de hook" 20 | 21 | DF_user="${DF_user:-$(_readaccountconf_mutable DF_user)}" 22 | DF_password="${DF_password:-$(_readaccountconf_mutable DF_password)}" 23 | if [ -z "$DF_user" ] || [ -z "$DF_password" ]; then 24 | DF_user="" 25 | DF_password="" 26 | _err "No auth details provided. Please set user credentials using the \$DF_user and \$DF_password environment variables." 27 | return 1 28 | fi 29 | #save the api user and password to the account conf file. 30 | _debug "Save user and password" 31 | _saveaccountconf_mutable DF_user "$DF_user" 32 | _saveaccountconf_mutable DF_password "$DF_password" 33 | 34 | domain="$(printf "%s" "$fulldomain" | cut -d"." -f2-)" 35 | 36 | get="$dyndnsfree_api?username=$DF_user&password=$DF_password&hostname=$domain&add_hostname=$fulldomain&txt=$txt_value" 37 | 38 | if ! erg="$(_get "$get")"; then 39 | _err "error Adding $fulldomain TXT: $txt_value" 40 | return 1 41 | fi 42 | 43 | if _contains "$erg" "success"; then 44 | _info "Success, TXT Added, OK" 45 | else 46 | _err "error Adding $fulldomain TXT: $txt_value erg: $erg" 47 | return 1 48 | fi 49 | 50 | _debug "ok Auto $fulldomain TXT: $txt_value erg: $erg" 51 | return 0 52 | } 53 | 54 | dns_df_rm() { 55 | 56 | fulldomain=$1 57 | txtvalue=$2 58 | _info "TXT enrty in $fulldomain is deleted automatically" 59 | _debug fulldomain "$fulldomain" 60 | _debug txtvalue "$txtvalue" 61 | 62 | } 63 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/dnsapi/dns_doapi.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC2034 3 | dns_doapi_info='Domain-Offensive do.de 4 | Official LetsEncrypt API for do.de / Domain-Offensive. 5 | This API is also available to private customers/individuals. 6 | Site: do.de 7 | Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_doapi 8 | Options: 9 | DO_LETOKEN LetsEncrypt Token 10 | Issues: github.com/acmesh-official/acme.sh/issues/2057 11 | ' 12 | 13 | DO_API="https://my.do.de/api/letsencrypt" 14 | 15 | ######## Public functions ##################### 16 | 17 | #Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" 18 | dns_doapi_add() { 19 | fulldomain=$1 20 | txtvalue=$2 21 | 22 | DO_LETOKEN="${DO_LETOKEN:-$(_readaccountconf_mutable DO_LETOKEN)}" 23 | if [ -z "$DO_LETOKEN" ]; then 24 | DO_LETOKEN="" 25 | _err "You didn't configure a do.de API token yet." 26 | _err "Please set DO_LETOKEN and try again." 27 | return 1 28 | fi 29 | _saveaccountconf_mutable DO_LETOKEN "$DO_LETOKEN" 30 | 31 | _info "Adding TXT record to ${fulldomain}" 32 | response="$(_get "$DO_API?token=$DO_LETOKEN&domain=${fulldomain}&value=${txtvalue}")" 33 | if _contains "${response}" 'success'; then 34 | return 0 35 | fi 36 | _err "Could not create resource record, check logs" 37 | _err "${response}" 38 | return 1 39 | } 40 | 41 | dns_doapi_rm() { 42 | fulldomain=$1 43 | 44 | DO_LETOKEN="${DO_LETOKEN:-$(_readaccountconf_mutable DO_LETOKEN)}" 45 | if [ -z "$DO_LETOKEN" ]; then 46 | DO_LETOKEN="" 47 | _err "You didn't configure a do.de API token yet." 48 | _err "Please set DO_LETOKEN and try again." 49 | return 1 50 | fi 51 | _saveaccountconf_mutable DO_LETOKEN "$DO_LETOKEN" 52 | 53 | _info "Deleting resource record $fulldomain" 54 | response="$(_get "$DO_API?token=$DO_LETOKEN&domain=${fulldomain}&action=delete")" 55 | if _contains "${response}" 'success'; then 56 | return 0 57 | fi 58 | _err "Could not delete resource record, check logs" 59 | _err "${response}" 60 | return 1 61 | } 62 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/dnsapi/dns_he_ddns.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC2034 3 | dns_he_ddns_info='Hurricane Electric HE.net DDNS 4 | Site: dns.he.net 5 | Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_he_ddns 6 | Options: 7 | HE_DDNS_KEY The DDNS key 8 | Author: Markku Leiniö 9 | ' 10 | 11 | HE_DDNS_URL="https://dyn.dns.he.net/nic/update" 12 | 13 | ######## Public functions ##################### 14 | 15 | #Usage: dns_he_ddns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" 16 | dns_he_ddns_add() { 17 | fulldomain=$1 18 | txtvalue=$2 19 | HE_DDNS_KEY="${HE_DDNS_KEY:-$(_readaccountconf_mutable HE_DDNS_KEY)}" 20 | if [ -z "$HE_DDNS_KEY" ]; then 21 | HE_DDNS_KEY="" 22 | _err "You didn't specify a DDNS key for accessing the TXT record in HE API." 23 | return 1 24 | fi 25 | #Save the DDNS key to the account conf file. 26 | _saveaccountconf_mutable HE_DDNS_KEY "$HE_DDNS_KEY" 27 | 28 | _info "Using Hurricane Electric DDNS API" 29 | _debug fulldomain "$fulldomain" 30 | _debug txtvalue "$txtvalue" 31 | 32 | response="$(_post "hostname=$fulldomain&password=$HE_DDNS_KEY&txt=$txtvalue" "$HE_DDNS_URL")" 33 | _info "Response: $response" 34 | _contains "$response" "good" && return 0 || return 1 35 | } 36 | 37 | # dns_he_ddns_rm() is not doing anything because the API call always updates the 38 | # contents of the existing record (that the API key gives access to). 39 | 40 | dns_he_ddns_rm() { 41 | fulldomain=$1 42 | _debug "Delete TXT record called for '${fulldomain}', not doing anything." 43 | return 0 44 | } 45 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/dnsapi/dns_myapi.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC2034 3 | dns_myapi_info='Custom API Example 4 | A sample custom DNS API script description. 5 | Domains: example.com example.net 6 | Site: github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide 7 | Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_myapi 8 | Options: 9 | MYAPI_Token API Token. Get API Token from https://example.com/api/ 10 | MYAPI_Variable2 Option 2. Default "default value". 11 | MYAPI_Variable2 Option 3. Optional. 12 | Issues: github.com/acmesh-official/acme.sh 13 | Author: Neil Pang 14 | ' 15 | 16 | #This file name is "dns_myapi.sh" 17 | #So, here must be a method dns_myapi_add() 18 | #Which will be called by acme.sh to add the txt record to your api system. 19 | #returns 0 means success, otherwise error. 20 | 21 | ######## Public functions ##################### 22 | 23 | # Please Read this guide first: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide 24 | 25 | #Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" 26 | dns_myapi_add() { 27 | fulldomain=$1 28 | txtvalue=$2 29 | _info "Using myapi" 30 | _debug fulldomain "$fulldomain" 31 | _debug txtvalue "$txtvalue" 32 | _err "Not implemented!" 33 | return 1 34 | } 35 | 36 | #Usage: fulldomain txtvalue 37 | #Remove the txt record after validation. 38 | dns_myapi_rm() { 39 | fulldomain=$1 40 | txtvalue=$2 41 | _info "Using myapi" 42 | _debug fulldomain "$fulldomain" 43 | _debug txtvalue "$txtvalue" 44 | } 45 | 46 | #################### Private functions below ################################## 47 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/dnsapi/dns_nanelo.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC2034 3 | dns_nanelo_info='Nanelo.com 4 | Site: Nanelo.com 5 | Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_nanelo 6 | Options: 7 | NANELO_TOKEN API Token 8 | Issues: github.com/acmesh-official/acme.sh/issues/4519 9 | ' 10 | 11 | NANELO_API="https://api.nanelo.com/v1/" 12 | 13 | ######## Public functions ##################### 14 | 15 | # Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" 16 | dns_nanelo_add() { 17 | fulldomain=$1 18 | txtvalue=$2 19 | 20 | NANELO_TOKEN="${NANELO_TOKEN:-$(_readaccountconf_mutable NANELO_TOKEN)}" 21 | if [ -z "$NANELO_TOKEN" ]; then 22 | NANELO_TOKEN="" 23 | _err "You didn't configure a Nanelo API Key yet." 24 | _err "Please set NANELO_TOKEN and try again." 25 | _err "Login to Nanelo.com and go to Settings > API Keys to get a Key" 26 | return 1 27 | fi 28 | _saveaccountconf_mutable NANELO_TOKEN "$NANELO_TOKEN" 29 | 30 | _info "Adding TXT record to ${fulldomain}" 31 | response="$(_get "$NANELO_API$NANELO_TOKEN/dns/addrecord?type=TXT&ttl=60&name=${fulldomain}&value=${txtvalue}")" 32 | if _contains "${response}" 'success'; then 33 | return 0 34 | fi 35 | _err "Could not create resource record, please check the logs" 36 | _err "${response}" 37 | return 1 38 | } 39 | 40 | dns_nanelo_rm() { 41 | fulldomain=$1 42 | txtvalue=$2 43 | 44 | NANELO_TOKEN="${NANELO_TOKEN:-$(_readaccountconf_mutable NANELO_TOKEN)}" 45 | if [ -z "$NANELO_TOKEN" ]; then 46 | NANELO_TOKEN="" 47 | _err "You didn't configure a Nanelo API Key yet." 48 | _err "Please set NANELO_TOKEN and try again." 49 | _err "Login to Nanelo.com and go to Settings > API Keys to get a Key" 50 | return 1 51 | fi 52 | _saveaccountconf_mutable NANELO_TOKEN "$NANELO_TOKEN" 53 | 54 | _info "Deleting resource record $fulldomain" 55 | response="$(_get "$NANELO_API$NANELO_TOKEN/dns/deleterecord?type=TXT&ttl=60&name=${fulldomain}&value=${txtvalue}")" 56 | if _contains "${response}" 'success'; then 57 | return 0 58 | fi 59 | _err "Could not delete resource record, please check the logs" 60 | _err "${response}" 61 | return 1 62 | } 63 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/dnsapi/dns_nsd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC2034 3 | dns_nsd_info='NLnetLabs NSD Server 4 | Site: github.com/NLnetLabs/nsd 5 | Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#nsd 6 | Options: 7 | Nsd_ZoneFile Zone File path. E.g. "/etc/nsd/zones/example.com.zone" 8 | Nsd_Command Command. E.g. "sudo nsd-control reload" 9 | Issues: github.com/acmesh-official/acme.sh/issues/2245 10 | ' 11 | 12 | # args: fulldomain txtvalue 13 | dns_nsd_add() { 14 | fulldomain=$1 15 | txtvalue=$2 16 | ttlvalue=300 17 | 18 | Nsd_ZoneFile="${Nsd_ZoneFile:-$(_readdomainconf Nsd_ZoneFile)}" 19 | Nsd_Command="${Nsd_Command:-$(_readdomainconf Nsd_Command)}" 20 | 21 | # Arg checks 22 | if [ -z "$Nsd_ZoneFile" ] || [ -z "$Nsd_Command" ]; then 23 | Nsd_ZoneFile="" 24 | Nsd_Command="" 25 | _err "Specify ENV vars Nsd_ZoneFile and Nsd_Command" 26 | return 1 27 | fi 28 | 29 | if [ ! -f "$Nsd_ZoneFile" ]; then 30 | Nsd_ZoneFile="" 31 | Nsd_Command="" 32 | _err "No such file: $Nsd_ZoneFile" 33 | return 1 34 | fi 35 | 36 | _savedomainconf Nsd_ZoneFile "$Nsd_ZoneFile" 37 | _savedomainconf Nsd_Command "$Nsd_Command" 38 | 39 | echo "$fulldomain. $ttlvalue IN TXT \"$txtvalue\"" >>"$Nsd_ZoneFile" 40 | _info "Added TXT record for $fulldomain" 41 | _debug "Running $Nsd_Command" 42 | if eval "$Nsd_Command"; then 43 | _info "Successfully updated the zone" 44 | return 0 45 | else 46 | _err "Problem updating the zone" 47 | return 1 48 | fi 49 | } 50 | 51 | # args: fulldomain txtvalue 52 | dns_nsd_rm() { 53 | fulldomain=$1 54 | txtvalue=$2 55 | ttlvalue=300 56 | 57 | Nsd_ZoneFile="${Nsd_ZoneFile:-$(_readdomainconf Nsd_ZoneFile)}" 58 | Nsd_Command="${Nsd_Command:-$(_readdomainconf Nsd_Command)}" 59 | 60 | _sed_i "/$fulldomain. $ttlvalue IN TXT \"$txtvalue\"/d" "$Nsd_ZoneFile" 61 | _info "Removed TXT record for $fulldomain" 62 | _debug "Running $Nsd_Command" 63 | if eval "$Nsd_Command"; then 64 | _info "Successfully reloaded NSD " 65 | return 0 66 | else 67 | _err "Problem reloading NSD" 68 | return 1 69 | fi 70 | } 71 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/dnsapi/dns_technitium.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC2034 3 | dns_technitium_info='Technitium DNS Server 4 | Site: Technitium.com/dns/ 5 | Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_technitium 6 | Options: 7 | Technitium_Server Server Address 8 | Technitium_Token API Token 9 | Issues: github.com/acmesh-official/acme.sh/issues/6116 10 | Author: Henning Reich 11 | ' 12 | 13 | dns_technitium_add() { 14 | _info "add txt Record using Technitium" 15 | _Technitium_account 16 | fulldomain=$1 17 | txtvalue=$2 18 | response="$(_get "$Technitium_Server/api/zones/records/add?token=$Technitium_Token&domain=$fulldomain&type=TXT&text=${txtvalue}")" 19 | if _contains "$response" '"status":"ok"'; then 20 | return 0 21 | fi 22 | _err "Could not add txt record." 23 | return 1 24 | } 25 | 26 | dns_technitium_rm() { 27 | _info "remove txt record using Technitium" 28 | _Technitium_account 29 | fulldomain=$1 30 | txtvalue=$2 31 | response="$(_get "$Technitium_Server/api/zones/records/delete?token=$Technitium_Token&domain=$fulldomain&type=TXT&text=${txtvalue}")" 32 | if _contains "$response" '"status":"ok"'; then 33 | return 0 34 | fi 35 | _err "Could not remove txt record" 36 | return 1 37 | } 38 | 39 | #################### Private functions below ################################## 40 | 41 | _Technitium_account() { 42 | Technitium_Server="${Technitium_Server:-$(_readaccountconf_mutable Technitium_Server)}" 43 | Technitium_Token="${Technitium_Token:-$(_readaccountconf_mutable Technitium_Token)}" 44 | if [ -z "$Technitium_Server" ] || [ -z "$Technitium_Token" ]; then 45 | Technitium_Server="" 46 | Technitium_Token="" 47 | _err "You don't specify Technitium Server and Token yet." 48 | _err "Please create your Token and add server address and try again." 49 | return 1 50 | fi 51 | 52 | #save the credentials to the account conf file. 53 | _saveaccountconf_mutable Technitium_Server "$Technitium_Server" 54 | _saveaccountconf_mutable Technitium_Token "$Technitium_Token" 55 | } 56 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/dnsapi/dns_tele3.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC2034 3 | dns_tele3_info='tele3.cz 4 | Site: tele3.cz 5 | Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#tele3 6 | Options: 7 | TELE3_Key API Key 8 | TELE3_Secret API Secret 9 | Author: Roman Blizik 10 | ' 11 | 12 | TELE3_API="https://www.tele3.cz/acme/" 13 | 14 | ######## Public functions ##################### 15 | 16 | dns_tele3_add() { 17 | _info "Using TELE3 DNS" 18 | data="\"ope\":\"add\", \"domain\":\"$1\", \"value\":\"$2\"" 19 | if ! _tele3_call; then 20 | _err "Publish zone failed" 21 | return 1 22 | fi 23 | 24 | _info "Zone published" 25 | } 26 | 27 | dns_tele3_rm() { 28 | _info "Using TELE3 DNS" 29 | data="\"ope\":\"rm\", \"domain\":\"$1\", \"value\":\"$2\"" 30 | if ! _tele3_call; then 31 | _err "delete TXT record failed" 32 | return 1 33 | fi 34 | 35 | _info "TXT record successfully deleted" 36 | } 37 | 38 | #################### Private functions below ################################## 39 | 40 | _tele3_init() { 41 | TELE3_Key="${TELE3_Key:-$(_readaccountconf_mutable TELE3_Key)}" 42 | TELE3_Secret="${TELE3_Secret:-$(_readaccountconf_mutable TELE3_Secret)}" 43 | if [ -z "$TELE3_Key" ] || [ -z "$TELE3_Secret" ]; then 44 | TELE3_Key="" 45 | TELE3_Secret="" 46 | _err "You must export variables: TELE3_Key and TELE3_Secret" 47 | return 1 48 | fi 49 | 50 | #save the config variables to the account conf file. 51 | _saveaccountconf_mutable TELE3_Key "$TELE3_Key" 52 | _saveaccountconf_mutable TELE3_Secret "$TELE3_Secret" 53 | } 54 | 55 | _tele3_call() { 56 | _tele3_init 57 | data="{\"key\":\"$TELE3_Key\", \"secret\":\"$TELE3_Secret\", $data}" 58 | 59 | _debug data "$data" 60 | 61 | response="$(_post "$data" "$TELE3_API" "" "POST")" 62 | _debug response "$response" 63 | 64 | if [ "$response" != "success" ]; then 65 | _err "$response" 66 | return 1 67 | fi 68 | } 69 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/dnsapi_cw/dns_df.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ABS_CURR_PATH=$(dirname $(realpath "${BASH_SOURCE[0]}")) 4 | SRC_FILE="${ABS_CURR_PATH}/../acme_src.sh" 5 | . "${SRC_FILE}" 6 | 7 | # shellcheck disable=SC2034 8 | dns_df_info='DynDnsFree.de 9 | Domains: dynup.de 10 | Site: DynDnsFree.de 11 | Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_df 12 | Options: 13 | DF_user Username 14 | DF_password Password 15 | Issues: github.com/acmesh-official/acme.sh/issues/2897 16 | Author: Thilo Gass 17 | ' 18 | 19 | dyndnsfree_api="https://dynup.de/acme.php" 20 | 21 | dns_df_add() { 22 | fulldomain=$1 23 | txt_value=$2 24 | _info "Using DNS-01 dyndnsfree.de hook" 25 | 26 | DF_user="${DF_user:-$(_readaccountconf_mutable DF_user)}" 27 | DF_password="${DF_password:-$(_readaccountconf_mutable DF_password)}" 28 | if [ -z "$DF_user" ] || [ -z "$DF_password" ]; then 29 | DF_user="" 30 | DF_password="" 31 | _err "No auth details provided. Please set user credentials using the \$DF_user and \$DF_password environment variables." 32 | return 1 33 | fi 34 | #save the api user and password to the account conf file. 35 | _debug "Save user and password" 36 | _saveaccountconf_mutable DF_user "$DF_user" 37 | _saveaccountconf_mutable DF_password "$DF_password" 38 | 39 | domain="$(printf "%s" "$fulldomain" | cut -d"." -f2-)" 40 | 41 | get="$dyndnsfree_api?username=$DF_user&password=$DF_password&hostname=$domain&add_hostname=$fulldomain&txt=$txt_value" 42 | 43 | if ! erg="$(_get "$get")"; then 44 | _err "error Adding $fulldomain TXT: $txt_value" 45 | return 1 46 | fi 47 | 48 | if _contains "$erg" "success"; then 49 | _info "Success, TXT Added, OK" 50 | else 51 | _err "error Adding $fulldomain TXT: $txt_value erg: $erg" 52 | return 1 53 | fi 54 | 55 | _debug "ok Auto $fulldomain TXT: $txt_value erg: $erg" 56 | return 0 57 | } 58 | 59 | dns_df_rm() { 60 | 61 | fulldomain=$1 62 | txtvalue=$2 63 | _info "TXT enrty in $fulldomain is deleted automatically" 64 | _debug fulldomain "$fulldomain" 65 | _debug txtvalue "$txtvalue" 66 | 67 | } 68 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/dnsapi_cw/dns_doapi.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ABS_CURR_PATH=$(dirname $(realpath "${BASH_SOURCE[0]}")) 4 | SRC_FILE="${ABS_CURR_PATH}/../acme_src.sh" 5 | . "${SRC_FILE}" 6 | 7 | # shellcheck disable=SC2034 8 | dns_doapi_info='Domain-Offensive do.de 9 | Official LetsEncrypt API for do.de / Domain-Offensive. 10 | This API is also available to private customers/individuals. 11 | Site: do.de 12 | Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_doapi 13 | Options: 14 | DO_LETOKEN LetsEncrypt Token 15 | Issues: github.com/acmesh-official/acme.sh/issues/2057 16 | ' 17 | 18 | DO_API="https://my.do.de/api/letsencrypt" 19 | 20 | ######## Public functions ##################### 21 | 22 | #Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" 23 | dns_doapi_add() { 24 | fulldomain=$1 25 | txtvalue=$2 26 | 27 | DO_LETOKEN="${DO_LETOKEN:-$(_readaccountconf_mutable DO_LETOKEN)}" 28 | if [ -z "$DO_LETOKEN" ]; then 29 | DO_LETOKEN="" 30 | _err "You didn't configure a do.de API token yet." 31 | _err "Please set DO_LETOKEN and try again." 32 | return 1 33 | fi 34 | _saveaccountconf_mutable DO_LETOKEN "$DO_LETOKEN" 35 | 36 | _info "Adding TXT record to ${fulldomain}" 37 | response="$(_get "$DO_API?token=$DO_LETOKEN&domain=${fulldomain}&value=${txtvalue}")" 38 | if _contains "${response}" 'success'; then 39 | return 0 40 | fi 41 | _err "Could not create resource record, check logs" 42 | _err "${response}" 43 | return 1 44 | } 45 | 46 | dns_doapi_rm() { 47 | fulldomain=$1 48 | 49 | DO_LETOKEN="${DO_LETOKEN:-$(_readaccountconf_mutable DO_LETOKEN)}" 50 | if [ -z "$DO_LETOKEN" ]; then 51 | DO_LETOKEN="" 52 | _err "You didn't configure a do.de API token yet." 53 | _err "Please set DO_LETOKEN and try again." 54 | return 1 55 | fi 56 | _saveaccountconf_mutable DO_LETOKEN "$DO_LETOKEN" 57 | 58 | _info "Deleting resource record $fulldomain" 59 | response="$(_get "$DO_API?token=$DO_LETOKEN&domain=${fulldomain}&action=delete")" 60 | if _contains "${response}" 'success'; then 61 | return 0 62 | fi 63 | _err "Could not delete resource record, check logs" 64 | _err "${response}" 65 | return 1 66 | } 67 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/dnsapi_cw/dns_he_ddns.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ABS_CURR_PATH=$(dirname $(realpath "${BASH_SOURCE[0]}")) 4 | SRC_FILE="${ABS_CURR_PATH}/../acme_src.sh" 5 | . "${SRC_FILE}" 6 | 7 | # shellcheck disable=SC2034 8 | dns_he_ddns_info='Hurricane Electric HE.net DDNS 9 | Site: dns.he.net 10 | Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_he_ddns 11 | Options: 12 | HE_DDNS_KEY The DDNS key 13 | Author: Markku Leiniö 14 | ' 15 | 16 | HE_DDNS_URL="https://dyn.dns.he.net/nic/update" 17 | 18 | ######## Public functions ##################### 19 | 20 | #Usage: dns_he_ddns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" 21 | dns_he_ddns_add() { 22 | fulldomain=$1 23 | txtvalue=$2 24 | HE_DDNS_KEY="${HE_DDNS_KEY:-$(_readaccountconf_mutable HE_DDNS_KEY)}" 25 | if [ -z "$HE_DDNS_KEY" ]; then 26 | HE_DDNS_KEY="" 27 | _err "You didn't specify a DDNS key for accessing the TXT record in HE API." 28 | return 1 29 | fi 30 | #Save the DDNS key to the account conf file. 31 | _saveaccountconf_mutable HE_DDNS_KEY "$HE_DDNS_KEY" 32 | 33 | _info "Using Hurricane Electric DDNS API" 34 | _debug fulldomain "$fulldomain" 35 | _debug txtvalue "$txtvalue" 36 | 37 | response="$(_post "hostname=$fulldomain&password=$HE_DDNS_KEY&txt=$txtvalue" "$HE_DDNS_URL")" 38 | _info "Response: $response" 39 | _contains "$response" "good" && return 0 || return 1 40 | } 41 | 42 | # dns_he_ddns_rm() is not doing anything because the API call always updates the 43 | # contents of the existing record (that the API key gives access to). 44 | 45 | dns_he_ddns_rm() { 46 | fulldomain=$1 47 | _debug "Delete TXT record called for '${fulldomain}', not doing anything." 48 | return 0 49 | } 50 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/dnsapi_cw/dns_myapi.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ABS_CURR_PATH=$(dirname $(realpath "${BASH_SOURCE[0]}")) 4 | SRC_FILE="${ABS_CURR_PATH}/../acme_src.sh" 5 | . "${SRC_FILE}" 6 | 7 | # shellcheck disable=SC2034 8 | dns_myapi_info='Custom API Example 9 | A sample custom DNS API script description. 10 | Domains: example.com example.net 11 | Site: github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide 12 | Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_myapi 13 | Options: 14 | MYAPI_Token API Token. Get API Token from https://example.com/api/ 15 | MYAPI_Variable2 Option 2. Default "default value". 16 | MYAPI_Variable2 Option 3. Optional. 17 | Issues: github.com/acmesh-official/acme.sh 18 | Author: Neil Pang 19 | ' 20 | 21 | #This file name is "dns_myapi.sh" 22 | #So, here must be a method dns_myapi_add() 23 | #Which will be called by acme.sh to add the txt record to your api system. 24 | #returns 0 means success, otherwise error. 25 | 26 | ######## Public functions ##################### 27 | 28 | # Please Read this guide first: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide 29 | 30 | #Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" 31 | dns_myapi_add() { 32 | fulldomain=$1 33 | txtvalue=$2 34 | _info "Using myapi" 35 | _debug fulldomain "$fulldomain" 36 | _debug txtvalue "$txtvalue" 37 | _err "Not implemented!" 38 | return 1 39 | } 40 | 41 | #Usage: fulldomain txtvalue 42 | #Remove the txt record after validation. 43 | dns_myapi_rm() { 44 | fulldomain=$1 45 | txtvalue=$2 46 | _info "Using myapi" 47 | _debug fulldomain "$fulldomain" 48 | _debug txtvalue "$txtvalue" 49 | } 50 | 51 | #################### Private functions below ################################## 52 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/dnsapi_cw/dns_nanelo.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ABS_CURR_PATH=$(dirname $(realpath "${BASH_SOURCE[0]}")) 4 | SRC_FILE="${ABS_CURR_PATH}/../acme_src.sh" 5 | . "${SRC_FILE}" 6 | 7 | # shellcheck disable=SC2034 8 | dns_nanelo_info='Nanelo.com 9 | Site: Nanelo.com 10 | Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_nanelo 11 | Options: 12 | NANELO_TOKEN API Token 13 | Issues: github.com/acmesh-official/acme.sh/issues/4519 14 | ' 15 | 16 | NANELO_API="https://api.nanelo.com/v1/" 17 | 18 | ######## Public functions ##################### 19 | 20 | # Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" 21 | dns_nanelo_add() { 22 | fulldomain=$1 23 | txtvalue=$2 24 | 25 | NANELO_TOKEN="${NANELO_TOKEN:-$(_readaccountconf_mutable NANELO_TOKEN)}" 26 | if [ -z "$NANELO_TOKEN" ]; then 27 | NANELO_TOKEN="" 28 | _err "You didn't configure a Nanelo API Key yet." 29 | _err "Please set NANELO_TOKEN and try again." 30 | _err "Login to Nanelo.com and go to Settings > API Keys to get a Key" 31 | return 1 32 | fi 33 | _saveaccountconf_mutable NANELO_TOKEN "$NANELO_TOKEN" 34 | 35 | _info "Adding TXT record to ${fulldomain}" 36 | response="$(_get "$NANELO_API$NANELO_TOKEN/dns/addrecord?type=TXT&ttl=60&name=${fulldomain}&value=${txtvalue}")" 37 | if _contains "${response}" 'success'; then 38 | return 0 39 | fi 40 | _err "Could not create resource record, please check the logs" 41 | _err "${response}" 42 | return 1 43 | } 44 | 45 | dns_nanelo_rm() { 46 | fulldomain=$1 47 | txtvalue=$2 48 | 49 | NANELO_TOKEN="${NANELO_TOKEN:-$(_readaccountconf_mutable NANELO_TOKEN)}" 50 | if [ -z "$NANELO_TOKEN" ]; then 51 | NANELO_TOKEN="" 52 | _err "You didn't configure a Nanelo API Key yet." 53 | _err "Please set NANELO_TOKEN and try again." 54 | _err "Login to Nanelo.com and go to Settings > API Keys to get a Key" 55 | return 1 56 | fi 57 | _saveaccountconf_mutable NANELO_TOKEN "$NANELO_TOKEN" 58 | 59 | _info "Deleting resource record $fulldomain" 60 | response="$(_get "$NANELO_API$NANELO_TOKEN/dns/deleterecord?type=TXT&ttl=60&name=${fulldomain}&value=${txtvalue}")" 61 | if _contains "${response}" 'success'; then 62 | return 0 63 | fi 64 | _err "Could not delete resource record, please check the logs" 65 | _err "${response}" 66 | return 1 67 | } 68 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/dnsapi_cw/dns_nsd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ABS_CURR_PATH=$(dirname $(realpath "${BASH_SOURCE[0]}")) 4 | SRC_FILE="${ABS_CURR_PATH}/../acme_src.sh" 5 | . "${SRC_FILE}" 6 | 7 | # shellcheck disable=SC2034 8 | dns_nsd_info='NLnetLabs NSD Server 9 | Site: github.com/NLnetLabs/nsd 10 | Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#nsd 11 | Options: 12 | Nsd_ZoneFile Zone File path. E.g. "/etc/nsd/zones/example.com.zone" 13 | Nsd_Command Command. E.g. "sudo nsd-control reload" 14 | Issues: github.com/acmesh-official/acme.sh/issues/2245 15 | ' 16 | 17 | # args: fulldomain txtvalue 18 | dns_nsd_add() { 19 | fulldomain=$1 20 | txtvalue=$2 21 | ttlvalue=300 22 | 23 | Nsd_ZoneFile="${Nsd_ZoneFile:-$(_readdomainconf Nsd_ZoneFile)}" 24 | Nsd_Command="${Nsd_Command:-$(_readdomainconf Nsd_Command)}" 25 | 26 | # Arg checks 27 | if [ -z "$Nsd_ZoneFile" ] || [ -z "$Nsd_Command" ]; then 28 | Nsd_ZoneFile="" 29 | Nsd_Command="" 30 | _err "Specify ENV vars Nsd_ZoneFile and Nsd_Command" 31 | return 1 32 | fi 33 | 34 | if [ ! -f "$Nsd_ZoneFile" ]; then 35 | Nsd_ZoneFile="" 36 | Nsd_Command="" 37 | _err "No such file: $Nsd_ZoneFile" 38 | return 1 39 | fi 40 | 41 | _savedomainconf Nsd_ZoneFile "$Nsd_ZoneFile" 42 | _savedomainconf Nsd_Command "$Nsd_Command" 43 | 44 | echo "$fulldomain. $ttlvalue IN TXT \"$txtvalue\"" >>"$Nsd_ZoneFile" 45 | _info "Added TXT record for $fulldomain" 46 | _debug "Running $Nsd_Command" 47 | if eval "$Nsd_Command"; then 48 | _info "Successfully updated the zone" 49 | return 0 50 | else 51 | _err "Problem updating the zone" 52 | return 1 53 | fi 54 | } 55 | 56 | # args: fulldomain txtvalue 57 | dns_nsd_rm() { 58 | fulldomain=$1 59 | txtvalue=$2 60 | ttlvalue=300 61 | 62 | Nsd_ZoneFile="${Nsd_ZoneFile:-$(_readdomainconf Nsd_ZoneFile)}" 63 | Nsd_Command="${Nsd_Command:-$(_readdomainconf Nsd_Command)}" 64 | 65 | _sed_i "/$fulldomain. $ttlvalue IN TXT \"$txtvalue\"/d" "$Nsd_ZoneFile" 66 | _info "Removed TXT record for $fulldomain" 67 | _debug "Running $Nsd_Command" 68 | if eval "$Nsd_Command"; then 69 | _info "Successfully reloaded NSD " 70 | return 0 71 | else 72 | _err "Problem reloading NSD" 73 | return 1 74 | fi 75 | } 76 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/dnsapi_cw/dns_technitium.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ABS_CURR_PATH=$(dirname $(realpath "${BASH_SOURCE[0]}")) 4 | SRC_FILE="${ABS_CURR_PATH}/../acme_src.sh" 5 | . "${SRC_FILE}" 6 | 7 | # shellcheck disable=SC2034 8 | dns_technitium_info='Technitium DNS Server 9 | Site: Technitium.com/dns/ 10 | Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_technitium 11 | Options: 12 | Technitium_Server Server Address 13 | Technitium_Token API Token 14 | Issues: github.com/acmesh-official/acme.sh/issues/6116 15 | Author: Henning Reich 16 | ' 17 | 18 | dns_technitium_add() { 19 | _info "add txt Record using Technitium" 20 | _Technitium_account 21 | fulldomain=$1 22 | txtvalue=$2 23 | response="$(_get "$Technitium_Server/api/zones/records/add?token=$Technitium_Token&domain=$fulldomain&type=TXT&text=${txtvalue}")" 24 | if _contains "$response" '"status":"ok"'; then 25 | return 0 26 | fi 27 | _err "Could not add txt record." 28 | return 1 29 | } 30 | 31 | dns_technitium_rm() { 32 | _info "remove txt record using Technitium" 33 | _Technitium_account 34 | fulldomain=$1 35 | txtvalue=$2 36 | response="$(_get "$Technitium_Server/api/zones/records/delete?token=$Technitium_Token&domain=$fulldomain&type=TXT&text=${txtvalue}")" 37 | if _contains "$response" '"status":"ok"'; then 38 | return 0 39 | fi 40 | _err "Could not remove txt record" 41 | return 1 42 | } 43 | 44 | #################### Private functions below ################################## 45 | 46 | _Technitium_account() { 47 | Technitium_Server="${Technitium_Server:-$(_readaccountconf_mutable Technitium_Server)}" 48 | Technitium_Token="${Technitium_Token:-$(_readaccountconf_mutable Technitium_Token)}" 49 | if [ -z "$Technitium_Server" ] || [ -z "$Technitium_Token" ]; then 50 | Technitium_Server="" 51 | Technitium_Token="" 52 | _err "You don't specify Technitium Server and Token yet." 53 | _err "Please create your Token and add server address and try again." 54 | return 1 55 | fi 56 | 57 | #save the credentials to the account conf file. 58 | _saveaccountconf_mutable Technitium_Server "$Technitium_Server" 59 | _saveaccountconf_mutable Technitium_Token "$Technitium_Token" 60 | } 61 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/dnsapi_cw/dns_tele3.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ABS_CURR_PATH=$(dirname $(realpath "${BASH_SOURCE[0]}")) 4 | SRC_FILE="${ABS_CURR_PATH}/../acme_src.sh" 5 | . "${SRC_FILE}" 6 | 7 | # shellcheck disable=SC2034 8 | dns_tele3_info='tele3.cz 9 | Site: tele3.cz 10 | Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#tele3 11 | Options: 12 | TELE3_Key API Key 13 | TELE3_Secret API Secret 14 | Author: Roman Blizik 15 | ' 16 | 17 | TELE3_API="https://www.tele3.cz/acme/" 18 | 19 | ######## Public functions ##################### 20 | 21 | dns_tele3_add() { 22 | _info "Using TELE3 DNS" 23 | data="\"ope\":\"add\", \"domain\":\"$1\", \"value\":\"$2\"" 24 | if ! _tele3_call; then 25 | _err "Publish zone failed" 26 | return 1 27 | fi 28 | 29 | _info "Zone published" 30 | } 31 | 32 | dns_tele3_rm() { 33 | _info "Using TELE3 DNS" 34 | data="\"ope\":\"rm\", \"domain\":\"$1\", \"value\":\"$2\"" 35 | if ! _tele3_call; then 36 | _err "delete TXT record failed" 37 | return 1 38 | fi 39 | 40 | _info "TXT record successfully deleted" 41 | } 42 | 43 | #################### Private functions below ################################## 44 | 45 | _tele3_init() { 46 | TELE3_Key="${TELE3_Key:-$(_readaccountconf_mutable TELE3_Key)}" 47 | TELE3_Secret="${TELE3_Secret:-$(_readaccountconf_mutable TELE3_Secret)}" 48 | if [ -z "$TELE3_Key" ] || [ -z "$TELE3_Secret" ]; then 49 | TELE3_Key="" 50 | TELE3_Secret="" 51 | _err "You must export variables: TELE3_Key and TELE3_Secret" 52 | return 1 53 | fi 54 | 55 | #save the config variables to the account conf file. 56 | _saveaccountconf_mutable TELE3_Key "$TELE3_Key" 57 | _saveaccountconf_mutable TELE3_Secret "$TELE3_Secret" 58 | } 59 | 60 | _tele3_call() { 61 | _tele3_init 62 | data="{\"key\":\"$TELE3_Key\", \"secret\":\"$TELE3_Secret\", $data}" 63 | 64 | _debug data "$data" 65 | 66 | response="$(_post "$data" "$TELE3_API" "" "POST")" 67 | _debug response "$response" 68 | 69 | if [ "$response" != "success" ]; then 70 | _err "$response" 71 | return 1 72 | fi 73 | } 74 | -------------------------------------------------------------------------------- /scripts/linux/acme.sh/v3.1.1.txt: -------------------------------------------------------------------------------- 1 | https://github.com/acmesh-official/acme.sh/releases/tag/3.1.1 2 | -------------------------------------------------------------------------------- /scripts/linux/certwarden.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Cert Warden 3 | 4 | [Service] 5 | Type=simple 6 | User=certwarden 7 | Restart=always 8 | RestartSec=5s 9 | WorkingDirectory=/opt/certwarden 10 | ExecStart=/opt/certwarden/certwarden 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /scripts/linux/create-dns.example.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | # Write your own script to create dns records 4 | 5 | echo "Some script that creates dns records" 6 | 7 | environment=$(printenv) 8 | echo "Environment: " 9 | echo "$environment" 10 | 11 | echo "" 12 | 13 | echo "Available Params:" 14 | echo "Record (1): " "$1" 15 | echo "Value (2): " "$2" 16 | -------------------------------------------------------------------------------- /scripts/linux/delete-dns.example.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | # Write your own script to delete dns records 4 | 5 | echo "Some script that deletes dns records" 6 | 7 | environment=$(printenv) 8 | echo "Environment: " 9 | echo "$environment" 10 | 11 | echo "" 12 | 13 | echo "Available Params:" 14 | echo "Record (1): " "$1" 15 | echo "Value (2): " "$2" 16 | -------------------------------------------------------------------------------- /scripts/linux/install.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | # install path and username 4 | install_path="/opt/certwarden" 5 | run_user="certwarden" 6 | 7 | 8 | # Check for root 9 | if [ "$(id -u)" -ne 0 ]; then echo "Please run as root." >&2; exit 1; fi 10 | 11 | # move to script path 12 | script_path=$(dirname $0) 13 | cd "$script_path" 14 | 15 | # create user to run app 16 | useradd -r -s /bin/false "$run_user" 17 | 18 | # copy all files to install path 19 | mkdir "$install_path" 20 | cp -R ../* "$install_path" 21 | 22 | # permissions 23 | ./set_permissions.sh "$install_path" "$run_user" 24 | 25 | # allow binding to low port numbers 26 | setcap CAP_NET_BIND_SERVICE=+eip /opt/certwarden/certwarden 27 | 28 | # install and start service 29 | cp ./certwarden.service /etc/systemd/system/certwarden.service 30 | systemctl daemon-reload 31 | systemctl enable certwarden 32 | systemctl start certwarden 33 | -------------------------------------------------------------------------------- /scripts/linux/post-processing.example.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | # Write your own script 4 | echo "Some script that post processes" 5 | 6 | # There are no default env variables anymore 7 | -------------------------------------------------------------------------------- /scripts/linux/set_permissions.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | # Default 4 | default_path="/opt/certwarden" 5 | default_user="certwarden" 6 | 7 | 8 | # allow command line specified 9 | install_path="${1:-$default_path}" 10 | run_user="${2:-$default_user}" 11 | 12 | # own entire folders / files 13 | chown "$run_user":"$run_user" "$install_path" -R 14 | 15 | # set to user/owner read/edit only but allow directory browsing 16 | find "$install_path" -type d -exec chmod 755 {} \; 17 | find "$install_path" -type f -exec chmod 640 {} \; 18 | 19 | # main executable 20 | chmod 750 "$install_path"/certwarden 21 | 22 | # allow execution of scripts 23 | find "$install_path"/scripts -type f -name "*.sh" -exec chmod 750 {} \; 24 | -------------------------------------------------------------------------------- /scripts/linux/upgrade.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | # install path and username 4 | install_path="/opt/certwarden" 5 | run_user="certwarden" 6 | 7 | 8 | # Check for root 9 | if [ "$(id -u)" -ne 0 ]; then echo "Please run as root." >&2; exit 1; fi 10 | 11 | # move to script path 12 | script_path=$(dirname $0) 13 | cd "$script_path" 14 | 15 | # stop 16 | systemctl stop certwarden 17 | 18 | # copy new files 19 | rm -r "$install_path"/frontend_build 20 | cp -R ../* "$install_path" 21 | 22 | # permissions 23 | ./set_permissions.sh "$install_path" "$run_user" 24 | 25 | # allow binding to low port numbers 26 | setcap CAP_NET_BIND_SERVICE=+eip /opt/certwarden/certwarden 27 | 28 | # restart service 29 | systemctl start certwarden 30 | -------------------------------------------------------------------------------- /scripts/windows/_warning_store_your_scripts_in_data_folder: -------------------------------------------------------------------------------- 1 | This folder is not backed up by the application. 2 | 3 | Store any customization somewhere in ./data/... 4 | -------------------------------------------------------------------------------- /scripts/windows/create-dns.example.ps1: -------------------------------------------------------------------------------- 1 | # Write your own script to create dns records 2 | 3 | Write-Host "Some script that creates dns records" 4 | 5 | Write-Host "Environment: $(Get-ChildItem env: | Out-String)" 6 | 7 | Write-Host "Available Params:" 8 | Write-Host "Record (args[0]): " $args[0] 9 | Write-Host "Value (args[1]): " $args[1] 10 | -------------------------------------------------------------------------------- /scripts/windows/delete-dns.example.ps1: -------------------------------------------------------------------------------- 1 | # Write your own script to delete dns records 2 | 3 | Write-Host "Some script that deletes dns records" 4 | 5 | Write-Host "Environment: $(Get-ChildItem env: | Out-String)" 6 | 7 | Write-Host "Available Params:" 8 | Write-Host "Record (args[0]): " $args[0] 9 | Write-Host "Value (args[1]): " $args[1] 10 | -------------------------------------------------------------------------------- /scripts/windows/post-processing.example.ps1: -------------------------------------------------------------------------------- 1 | # Write your own script 2 | 3 | Write-Host "Some script that post processes after order is valid." 4 | 5 | # There are no default env variables anymore 6 | --------------------------------------------------------------------------------