Skip to content

Instantly share code, notes, and snippets.

@mholt
Created February 22, 2021 22:37
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mholt/f3a7277e54b26938401638d5786cd0d6 to your computer and use it in GitHub Desktop.
Save mholt/f3a7277e54b26938401638d5786cd0d6 to your computer and use it in GitHub Desktop.
Unsupported, ad-hoc program that migrates assets from Caddy v1 to Caddy v2
// Copyright 2021 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package main migrates certificate assets from old Caddy v1 installations
// to the structure and formats used by Caddy v2. **ALWAYS BACK UP YOUR FILES
// BEFORE RUNNING THIS PROGRAM.** Although this code was known to be working
// before it was extracted and removed from Caddy (and CertMagic), there are
// no guarantees it will work correctly in all edge cases. Please be careful!
// This program is offered as a courtesy only, and it's not really something
// I'm interested in maintaining or supporting. For most users, it's usually
// acceptable to just have Caddy 2 regenerate necessary assets one time; and
// this program is probably not needed.
//
// More information: https://github.com/caddyserver/caddy/issues/2955
//
// For command help, run this program with the `-h` flag. Be sure your
// user and environment (especially variables) is properly configured!
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
)
var (
inFolder = flag.String("in", oldCaddyPath(), "Input folder path (old CADDYPATH)")
outFolder = flag.String("out", caddy.AppDataDir(), "Output folder path")
accounts = flag.Bool("accounts", false, "Migrate accounts (RFC 8555 only; not compatible with ACMEv1)")
)
func main() {
flag.Parse()
if err := migrate(*inFolder, *outFolder); err != nil {
log.Fatal(err)
}
}
func migrate(inFolder, outFolder string) error {
if inFolder == "" || outFolder == "" {
return fmt.Errorf("input and output folders are both required")
}
inFolder = filepath.Clean(inFolder)
outFolder = filepath.Clean(outFolder)
if inFolder == outFolder {
return fmt.Errorf("input and output folders cannot be the same")
}
var err error
inFolder, err = filepath.Abs(inFolder)
if err != nil {
return err
}
outFolder, err = filepath.Abs(outFolder)
if err != nil {
return err
}
if strings.HasPrefix(outFolder, inFolder) ||
strings.HasPrefix(inFolder, outFolder) {
return fmt.Errorf("input and output folders cannot be within each other")
}
caFolders, err := listFolders(filepath.Join(inFolder, "acme"))
if err != nil {
return err
}
for _, caFolder := range caFolders {
err := moveCertificates(caFolder, outFolder)
if err != nil {
return err
}
if *accounts {
err = moveAccounts(caFolder, outFolder)
if err != nil {
return err
}
}
}
return nil
}
func moveCertificates(oldCA, dest string) error {
// make new destination path
newBaseDir, err := makeNewCADir(dest, "certificates", oldCA)
if err != nil {
return err
}
// list sites in old path
sitesDir := filepath.Join(oldCA, "sites")
sites, err := listFolders(sitesDir)
if err != nil {
return err
}
// for each site, copy its folder and re-encode its metadata
for _, site := range sites {
siteAssets, err := ioutil.ReadDir(site)
if err != nil {
return err
}
for _, siteAsset := range siteAssets {
oldPath := filepath.Join(site, siteAsset.Name())
newPath := filepath.Join(newBaseDir, filepath.Base(site), siteAsset.Name())
log.Printf("copying %s -> %s", oldPath, newPath)
err := copyFile(oldPath, newPath)
if err != nil {
return err
}
// re-encode metadata file
if filepath.Ext(newPath) == ".json" {
metaContents, err := ioutil.ReadFile(newPath)
if err != nil {
return err
}
if len(metaContents) == 0 {
continue
}
var oldMeta struct {
Domain string `json:"domain"`
}
err = json.Unmarshal(metaContents, &oldMeta)
if err != nil {
return err
}
cr := certmagic.CertificateResource{
SANs: []string{oldMeta.Domain},
IssuerData: json.RawMessage(metaContents),
}
newMeta, err := json.MarshalIndent(cr, "", "\t")
if err != nil {
return err
}
err = ioutil.WriteFile(newPath, newMeta, 0600)
if err != nil {
return err
}
}
}
}
return nil
}
func moveAccounts(oldCA, dest string) error {
newBaseDir, err := makeNewCADir(dest, "acme", oldCA)
if err != nil {
return err
}
// list users in old path
usersDir := filepath.Join(oldCA, "users")
users, err := listFolders(usersDir)
if err != nil {
return err
}
// for each user, copy its folder and re-encode its metadata
for _, userDir := range users {
userAssets, err := ioutil.ReadDir(userDir)
if err != nil {
return err
}
for _, userAsset := range userAssets {
oldPath := filepath.Join(userDir, userAsset.Name())
newPath := filepath.Join(newBaseDir, filepath.Base(userDir), userAsset.Name())
log.Printf("copying %s -> %s", oldPath, newPath)
err := copyFile(oldPath, newPath)
if err != nil {
return err
}
// re-encode metadata file
if filepath.Ext(newPath) == ".json" {
metaContents, err := ioutil.ReadFile(newPath)
if err != nil {
return err
}
if len(metaContents) == 0 {
continue
}
var newAcct acme.Account
err = json.Unmarshal(metaContents, &newAcct)
if err != nil {
return err
}
if newAcct.Status != "" && newAcct.Location != "" {
continue
}
var oldAcct struct {
Email string `json:"Email"`
Registration struct {
Body struct {
Status string `json:"status"`
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
Orders string `json:"orders"`
ExternalAccountBinding json.RawMessage `json:"externalAccountBinding"`
} `json:"body"`
URI string `json:"uri"`
} `json:"Registration"`
}
err = json.Unmarshal(metaContents, &oldAcct)
if err != nil {
return fmt.Errorf("decoding into old account type: %v", err)
}
newAcct.Status = oldAcct.Registration.Body.Status
newAcct.TermsOfServiceAgreed = oldAcct.Registration.Body.TermsOfServiceAgreed
newAcct.Location = oldAcct.Registration.URI
newAcct.ExternalAccountBinding = oldAcct.Registration.Body.ExternalAccountBinding
newAcct.Orders = oldAcct.Registration.Body.Orders
if oldAcct.Email != "" {
newAcct.Contact = []string{"mailto:" + oldAcct.Email}
}
newMeta, err := json.MarshalIndent(newAcct, "", "\t")
if err != nil {
return err
}
err = ioutil.WriteFile(newPath, newMeta, 0600)
if err != nil {
return err
}
}
}
}
return nil
}
func makeNewCADir(dest, subfolder, oldCA string) (string, error) {
newCAName := filepath.Base(oldCA)
if strings.Contains(oldCA, "api.letsencrypt.org") {
if !strings.HasSuffix(oldCA, "-directory") {
newCAName += "-directory"
}
newCAName = strings.Replace(newCAName, "acme-v01.", "acme-v02.", 1)
}
newBaseDir := filepath.Join(dest, subfolder, newCAName)
err := os.MkdirAll(newBaseDir, 0700)
if err != nil {
return newBaseDir, fmt.Errorf("making new CA directory: %v", err)
}
return newBaseDir, nil
}
func copyFile(src, dest string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
err = os.MkdirAll(filepath.Dir(dest), 0700)
if err != nil {
return err
}
destFile, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer destFile.Close()
if _, err = io.Copy(destFile, srcFile); err != nil {
return err
}
return destFile.Sync()
}
// listFolders lists the subfolders in path.
func listFolders(path string) ([]string, error) {
files, err := ioutil.ReadDir(path)
if err != nil {
return nil, err
}
var subfolders []string
for _, f := range files {
if !f.IsDir() {
continue
}
subfolders = append(subfolders, filepath.Join(path, f.Name()))
}
return subfolders, nil
}
// oldCaddyPath returns the default, old Caddy path if they exist,
// which either could have been either $HOME/.caddy (really old,
// pre-v1) or $HOME/.local/share/caddy, or empty string if neither
// exists.
func oldCaddyPath() string {
home := homeDir()
for _, oldPath := range []string{
filepath.Join(home, ".local", "share", "caddy"),
filepath.Join(home, ".caddy"),
} {
if fileExists(oldPath) {
return oldPath
}
}
return ""
}
// homeDir gets the home directory the v1 way.
func homeDir() string {
home := os.Getenv("HOME")
if home == "" && runtime.GOOS == "windows" {
drive := os.Getenv("HOMEDRIVE")
path := os.Getenv("HOMEPATH")
home = drive + path
if drive == "" || path == "" {
home = os.Getenv("USERPROFILE")
}
}
if home == "" {
home = "."
}
return home
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment