1package zip 2 3import ( 4 "crypto/sha256" 5 "encoding/hex" 6 "fmt" 7 "io" 8 "net/http" 9 "net/http/httptest" 10 "os" 11 "testing" 12 "time" 13 14 "github.com/stretchr/testify/require" 15 16 "gitlab.com/gitlab-org/gitlab-pages/internal/config" 17 "gitlab.com/gitlab-org/gitlab-pages/internal/serving" 18 "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" 19) 20 21func TestZip_ServeFileHTTP(t *testing.T) { 22 testServerURL, cleanup := newZipFileServerURL(t, "group/zip.gitlab.io/public-without-dirs.zip") 23 defer cleanup() 24 25 wd, err := os.Getwd() 26 require.NoError(t, err) 27 28 httpURL := testServerURL + "/public.zip" 29 fileURL := "file://" + wd + "/group/zip.gitlab.io/public-without-dirs.zip" 30 31 tests := map[string]struct { 32 vfsPath string 33 path string 34 expectedStatus int 35 expectedBody string 36 extraHeaders http.Header 37 }{ 38 "accessing /index.html": { 39 vfsPath: httpURL, 40 path: "/index.html", 41 expectedStatus: http.StatusOK, 42 expectedBody: "zip.gitlab.io/project/index.html\n", 43 }, 44 "accessing /index.html from disk": { 45 vfsPath: fileURL, 46 path: "/index.html", 47 expectedStatus: http.StatusOK, 48 expectedBody: "zip.gitlab.io/project/index.html\n", 49 }, 50 "accessing /": { 51 vfsPath: httpURL, 52 path: "/", 53 expectedStatus: http.StatusOK, 54 expectedBody: "zip.gitlab.io/project/index.html\n", 55 }, 56 "accessing / If-Modified-Since": { 57 vfsPath: httpURL, 58 path: "/", 59 expectedStatus: http.StatusNotModified, 60 extraHeaders: http.Header{ 61 "If-Modified-Since": {time.Now().Format(http.TimeFormat)}, 62 }, 63 }, 64 "accessing / If-Modified-Since fails": { 65 vfsPath: httpURL, 66 path: "/", 67 expectedStatus: http.StatusOK, 68 expectedBody: "zip.gitlab.io/project/index.html\n", 69 extraHeaders: http.Header{ 70 "If-Modified-Since": {time.Now().AddDate(-10, 0, 0).Format(http.TimeFormat)}, 71 }, 72 }, 73 "accessing / If-Unmodified-Since": { 74 vfsPath: httpURL, 75 path: "/", 76 expectedStatus: http.StatusPreconditionFailed, 77 extraHeaders: http.Header{ 78 "If-Unmodified-Since": {time.Now().AddDate(-10, 0, 0).Format(http.TimeFormat)}, 79 }, 80 }, 81 "accessing / If-Unmodified-Since fails": { 82 vfsPath: httpURL, 83 path: "/", 84 expectedStatus: http.StatusOK, 85 expectedBody: "zip.gitlab.io/project/index.html\n", 86 extraHeaders: http.Header{ 87 "If-Unmodified-Since": {time.Now().Format(http.TimeFormat)}, 88 }, 89 }, 90 "accessing / from disk": { 91 vfsPath: fileURL, 92 path: "/", 93 expectedStatus: http.StatusOK, 94 expectedBody: "zip.gitlab.io/project/index.html\n", 95 }, 96 "accessing without /": { 97 vfsPath: httpURL, 98 path: "", 99 expectedStatus: http.StatusFound, 100 expectedBody: "<a href=\"//zip.gitlab.io/zip/\">Found</a>.\n\n", 101 }, 102 "accessing without / from disk": { 103 vfsPath: fileURL, 104 path: "", 105 expectedStatus: http.StatusFound, 106 expectedBody: "<a href=\"//zip.gitlab.io/zip/\">Found</a>.\n\n", 107 }, 108 "accessing archive that is 404": { 109 vfsPath: testServerURL + "/invalid.zip", 110 path: "/index.html", 111 // we expect the status to not be set 112 expectedStatus: 0, 113 }, 114 "accessing archive that is 500": { 115 vfsPath: testServerURL + "/500", 116 path: "/index.html", 117 expectedStatus: http.StatusInternalServerError, 118 }, 119 "accessing file:// outside of allowedPaths": { 120 vfsPath: "file:///some/file/outside/path", 121 path: "/index.html", 122 expectedStatus: http.StatusInternalServerError, 123 }, 124 "accessing / If-None-Match": { 125 vfsPath: httpURL, 126 path: "/", 127 expectedStatus: http.StatusNotModified, 128 extraHeaders: http.Header{ 129 "If-None-Match": {fmt.Sprintf("%q", sha(httpURL))}, 130 }, 131 }, 132 "accessing / If-None-Match fails": { 133 vfsPath: httpURL, 134 path: "/", 135 expectedStatus: http.StatusOK, 136 expectedBody: "zip.gitlab.io/project/index.html\n", 137 extraHeaders: http.Header{ 138 "If-None-Match": {fmt.Sprintf("%q", "badetag")}, 139 }, 140 }, 141 "accessing / If-Match": { 142 vfsPath: httpURL, 143 path: "/", 144 expectedStatus: http.StatusOK, 145 expectedBody: "zip.gitlab.io/project/index.html\n", 146 extraHeaders: http.Header{ 147 "If-Match": {fmt.Sprintf("%q", sha(httpURL))}, 148 }, 149 }, 150 "accessing / If-Match fails": { 151 vfsPath: httpURL, 152 path: "/", 153 expectedStatus: http.StatusPreconditionFailed, 154 extraHeaders: http.Header{ 155 "If-Match": {fmt.Sprintf("%q", "wrongetag")}, 156 }, 157 }, 158 "accessing / If-Match fails2": { 159 vfsPath: httpURL, 160 path: "/", 161 expectedStatus: http.StatusPreconditionFailed, 162 extraHeaders: http.Header{ 163 "If-Match": {","}, 164 }, 165 }, 166 } 167 168 cfg := &config.Config{ 169 Zip: config.ZipServing{ 170 ExpirationInterval: 10 * time.Second, 171 CleanupInterval: 5 * time.Second, 172 RefreshInterval: 5 * time.Second, 173 OpenTimeout: 5 * time.Second, 174 AllowedPaths: []string{wd}, 175 }, 176 } 177 178 s := Instance() 179 err = s.Reconfigure(cfg) 180 require.NoError(t, err) 181 182 for name, test := range tests { 183 t.Run(name, func(t *testing.T) { 184 w := httptest.NewRecorder() 185 w.Code = 0 // ensure that code is not set, and it is being set by handler 186 r := httptest.NewRequest(http.MethodGet, "http://zip.gitlab.io/zip"+test.path, nil) 187 188 if test.extraHeaders != nil { 189 r.Header = test.extraHeaders 190 } 191 192 handler := serving.Handler{ 193 Writer: w, 194 Request: r, 195 LookupPath: &serving.LookupPath{ 196 Prefix: "/zip/", 197 Path: test.vfsPath, 198 SHA256: sha(test.vfsPath), 199 }, 200 SubPath: test.path, 201 } 202 203 if test.expectedStatus == 0 { 204 require.False(t, s.ServeFileHTTP(handler)) 205 require.Zero(t, w.Code, "we expect status to not be set") 206 return 207 } 208 209 require.True(t, s.ServeFileHTTP(handler)) 210 211 resp := w.Result() 212 defer resp.Body.Close() 213 214 require.Equal(t, test.expectedStatus, resp.StatusCode) 215 body, err := io.ReadAll(resp.Body) 216 require.NoError(t, err) 217 218 if test.expectedStatus == http.StatusOK { 219 require.NotEmpty(t, resp.Header.Get("Last-Modified")) 220 require.NotEmpty(t, resp.Header.Get("ETag")) 221 } 222 223 if test.expectedStatus != http.StatusInternalServerError { 224 require.Equal(t, test.expectedBody, string(body)) 225 } 226 }) 227 } 228} 229 230func sha(path string) string { 231 sha := sha256.Sum256([]byte(path)) 232 s := hex.EncodeToString(sha[:]) 233 return s 234} 235 236var chdirSet = false 237 238func newZipFileServerURL(t *testing.T, zipFilePath string) (string, func()) { 239 t.Helper() 240 241 chdir := testhelpers.ChdirInPath(t, "../../../../shared/pages", &chdirSet) 242 243 m := http.NewServeMux() 244 m.HandleFunc("/public.zip", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 245 http.ServeFile(w, r, zipFilePath) 246 })) 247 m.HandleFunc("/500", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 248 w.WriteHeader(http.StatusInternalServerError) 249 })) 250 251 testServer := httptest.NewServer(m) 252 253 return testServer.URL, func() { 254 chdir() 255 testServer.Close() 256 } 257} 258