[mirror] Command-line application for uploading a site to a git-pages server
1package main
2
3import (
4 "archive/tar"
5 "bytes"
6 "crypto/sha256"
7 "fmt"
8 "io"
9 "io/fs"
10 "net/http"
11 "net/url"
12 "os"
13
14 "github.com/google/uuid"
15 "github.com/klauspost/compress/zstd"
16 "github.com/spf13/pflag"
17)
18
19var passwordFlag = pflag.String("password", "", "password for DNS challenge authorization")
20var challengeFlag = pflag.Bool("challenge", false, "compute DNS challenge entry from password (output zone file record)")
21var challengeBareFlag = pflag.Bool("challenge-bare", false, "compute DNS challenge entry from password (output bare TXT value)")
22var uploadGitFlag = pflag.String("upload-git", "", "replace site with contents of specified git repository")
23var uploadDirFlag = pflag.String("upload-dir", "", "replace site with contents of specified directory")
24var deleteFlag = pflag.Bool("delete", false, "delete site")
25var debugManifestFlag = pflag.Bool("debug-manifest", false, "retrieve site manifest as ProtoJSON, for debugging")
26var serverFlag = pflag.String("server", "", "hostname of server to connect to")
27var verboseFlag = pflag.Bool("verbose", false, "display more information for debugging")
28
29func singleOperation() bool {
30 operations := 0
31 if *challengeFlag {
32 operations++
33 }
34 if *challengeBareFlag {
35 operations++
36 }
37 if *uploadDirFlag != "" {
38 operations++
39 }
40 if *uploadGitFlag != "" {
41 operations++
42 }
43 if *deleteFlag {
44 operations++
45 }
46 if *debugManifestFlag {
47 operations++
48 }
49 return operations == 1
50}
51
52func displayFS(root fs.FS) error {
53 return fs.WalkDir(root, ".", func(name string, entry fs.DirEntry, err error) error {
54 if err != nil {
55 return err
56 }
57 switch {
58 case entry.Type() == 0:
59 fmt.Fprintln(os.Stderr, "file", name)
60 case entry.Type() == fs.ModeDir:
61 fmt.Fprintln(os.Stderr, "dir", name)
62 case entry.Type() == fs.ModeSymlink:
63 fmt.Fprintln(os.Stderr, "symlink", name)
64 default:
65 fmt.Fprintln(os.Stderr, "other", name)
66 }
67 return nil
68 })
69}
70
71func archiveFS(root fs.FS) (result []byte, err error) {
72 buffer := bytes.Buffer{}
73 zstdWriter, _ := zstd.NewWriter(&buffer)
74 tarWriter := tar.NewWriter(zstdWriter)
75 err = tarWriter.AddFS(root)
76 if err != nil {
77 return
78 }
79 err = tarWriter.Close()
80 if err != nil {
81 return
82 }
83 err = zstdWriter.Close()
84 if err != nil {
85 return
86 }
87 result = buffer.Bytes()
88 return
89}
90
91func main() {
92 pflag.Parse()
93 if !singleOperation() || len(pflag.Args()) != 1 {
94 fmt.Fprintf(os.Stderr,
95 "Usage: %s <site-url> [--challenge|--upload-git url|--upload-dir path|--delete]\n",
96 os.Args[0],
97 )
98 os.Exit(125)
99 }
100
101 var err error
102 siteURL, err := url.Parse(pflag.Args()[0])
103 if err != nil {
104 fmt.Fprintf(os.Stderr, "error: invalid site URL: %s\n", err)
105 os.Exit(1)
106 }
107
108 var request *http.Request
109 switch {
110 case *challengeFlag || *challengeBareFlag:
111 if *passwordFlag == "" {
112 *passwordFlag = uuid.NewString()
113 fmt.Fprintf(os.Stderr, "password: %s\n", *passwordFlag)
114 }
115
116 challenge := sha256.Sum256(fmt.Appendf(nil, "%s %s", siteURL.Hostname(), *passwordFlag))
117 if *challengeBareFlag {
118 fmt.Fprintf(os.Stdout, "%x\n", challenge)
119 } else {
120 fmt.Fprintf(os.Stdout, "_git-pages-challenge.%s. 3600 IN TXT \"%x\"\n", siteURL.Hostname(), challenge)
121 }
122 os.Exit(0)
123
124 case *uploadGitFlag != "":
125 uploadGitUrl, err := url.Parse(*uploadGitFlag)
126 if err != nil {
127 fmt.Fprintf(os.Stderr, "error: invalid repository URL: %s\n", err)
128 os.Exit(1)
129 }
130
131 requestBody := []byte(uploadGitUrl.String())
132 request, err = http.NewRequest("PUT", siteURL.String(), bytes.NewReader(requestBody))
133 if err != nil {
134 fmt.Fprintf(os.Stderr, "error: %s\n", err)
135 os.Exit(1)
136 }
137 request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
138
139 case *uploadDirFlag != "":
140 uploadDirFS, err := os.OpenRoot(*uploadDirFlag)
141 if err != nil {
142 fmt.Fprintf(os.Stderr, "error: invalid directory: %s\n", err)
143 os.Exit(1)
144 }
145
146 if *verboseFlag {
147 err := displayFS(uploadDirFS.FS())
148 if err != nil {
149 fmt.Fprintf(os.Stderr, "error: %s\n", err)
150 os.Exit(1)
151 }
152 }
153
154 requestBody, err := archiveFS(uploadDirFS.FS())
155 if err != nil {
156 fmt.Fprintf(os.Stderr, "error: %s\n", err)
157 os.Exit(1)
158 }
159
160 request, err = http.NewRequest("PUT", siteURL.String(), bytes.NewReader(requestBody))
161 if err != nil {
162 fmt.Fprintf(os.Stderr, "error: %s\n", err)
163 os.Exit(1)
164 }
165 request.Header.Add("Content-Type", "application/x-tar+zstd")
166
167 case *deleteFlag:
168 request, err = http.NewRequest("DELETE", siteURL.String(), bytes.NewReader([]byte{}))
169 if err != nil {
170 fmt.Fprintf(os.Stderr, "error: %s\n", err)
171 os.Exit(1)
172 }
173
174 case *debugManifestFlag:
175 manifestURL := siteURL.ResolveReference(&url.URL{Path: ".git-pages/manifest.json"})
176 request, err = http.NewRequest("GET", manifestURL.String(), bytes.NewReader([]byte{}))
177 if err != nil {
178 fmt.Fprintf(os.Stderr, "error: %s\n", err)
179 os.Exit(1)
180 }
181
182 default:
183 panic("no operation chosen")
184 }
185 if *passwordFlag != "" {
186 request.Header.Add("Authorization", fmt.Sprintf("Pages %s", *passwordFlag))
187 }
188 if *serverFlag != "" {
189 // Send the request to `--server` host, but set the `Host:` header to the site host.
190 // This allows first-time publishing to proceed without the git-pages server yet having
191 // a TLS certificate for the site host (which has a circular dependency on completion of
192 // first-time publishing).
193 newURL := *request.URL
194 newURL.Host = *serverFlag
195 request.URL = &newURL
196 request.Header.Set("Host", siteURL.Host)
197 }
198
199 response, err := http.DefaultClient.Do(request)
200 if err != nil {
201 fmt.Fprintf(os.Stderr, "error: %s\n", err)
202 os.Exit(1)
203 }
204 if *verboseFlag {
205 fmt.Fprintf(os.Stderr, "server: %s\n", response.Header.Get("Server"))
206 }
207 if *debugManifestFlag {
208 if response.StatusCode == 200 {
209 io.Copy(os.Stdout, response.Body)
210 fmt.Fprintf(os.Stdout, "\n")
211 } else {
212 io.Copy(os.Stderr, response.Body)
213 os.Exit(1)
214 }
215 } else { // an update operation
216 if response.StatusCode == 200 {
217 fmt.Fprintf(os.Stdout, "result: %s\n", response.Header.Get("Update-Result"))
218 io.Copy(os.Stdout, response.Body)
219 } else {
220 fmt.Fprintf(os.Stderr, "result: error\n")
221 io.Copy(os.Stderr, response.Body)
222 os.Exit(1)
223 }
224 }
225}