Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 01:00 UTC
| 1 | // Copyright 2015 Matthew Holt and The Caddy Authors |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | package fileserver |
| 16 | |
| 17 | import ( |
| 18 | "encoding/json" |
| 19 | "fmt" |
| 20 | "io" |
| 21 | "log" |
| 22 | "os" |
| 23 | "strconv" |
| 24 | "time" |
| 25 | |
| 26 | "github.com/caddyserver/certmagic" |
| 27 | "github.com/spf13/cobra" |
| 28 | "go.uber.org/zap" |
| 29 | |
| 30 | caddycmd "github.com/caddyserver/caddy/v2/cmd" |
| 31 | |
| 32 | "github.com/caddyserver/caddy/v2" |
| 33 | "github.com/caddyserver/caddy/v2/caddyconfig" |
| 34 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" |
| 35 | "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" |
| 36 | caddytpl "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates" |
| 37 | ) |
| 38 | |
| 39 | func init() { |
| 40 | caddycmd.RegisterCommand(caddycmd.Command{ |
| 41 | Name: "file-server", |
| 42 | Usage: "[--domain <example.com>] [--root <path>] [--listen <addr>] [--browse] [--reveal-symlinks] [--access-log] [--precompressed]", |
| 43 | Short: "Spins up a production-ready file server", |
| 44 | Long: ` |
| 45 | A simple but production-ready file server. Useful for quick deployments, |
| 46 | demos, and development. |
| 47 | |
| 48 | The listener's socket address can be customized with the --listen flag. |
| 49 | |
| 50 | If a domain name is specified with --domain, the default listener address |
| 51 | will be changed to the HTTPS port and the server will use HTTPS. If using |
| 52 | a public domain, ensure A/AAAA records are properly configured before |
| 53 | using this option. |
| 54 | |
| 55 | By default, Zstandard and Gzip compression are enabled. Use --no-compress |
| 56 | to disable compression. |
| 57 | |
| 58 | If --browse is enabled, requests for folders without an index file will |
| 59 | respond with a file listing.`, |
| 60 | CobraFunc: func(cmd *cobra.Command) { |
| 61 | cmd.Flags().StringP("domain", "d", "", "Domain name at which to serve the files") |
| 62 | cmd.Flags().StringP("root", "r", "", "The path to the root of the site") |
| 63 | cmd.Flags().StringP("listen", "l", "", "The address to which to bind the listener") |
| 64 | cmd.Flags().BoolP("browse", "b", false, "Enable directory browsing") |
| 65 | cmd.Flags().BoolP("reveal-symlinks", "", false, "Show symlink paths when browse is enabled.") |
| 66 | cmd.Flags().BoolP("templates", "t", false, "Enable template rendering") |
| 67 | cmd.Flags().BoolP("access-log", "a", false, "Enable the access log") |
| 68 | cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs") |
| 69 | cmd.Flags().IntP("file-limit", "f", defaultDirEntryLimit, "Max directories to read") |
| 70 | cmd.Flags().BoolP("no-compress", "", false, "Disable Zstandard and Gzip compression") |
| 71 | cmd.Flags().StringSliceP("precompressed", "p", []string{}, "Specify precompression file extensions. Compression preference implied from flag order.") |
| 72 | cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdFileServer) |
| 73 | cmd.AddCommand(&cobra.Command{ |
| 74 | Use: "export-template", |
| 75 | Short: "Exports the default file browser template", |
| 76 | Example: "caddy file-server export-template > browse.html", |
| 77 | RunE: func(cmd *cobra.Command, args []string) error { |
| 78 | _, err := io.WriteString(os.Stdout, BrowseTemplate) |
| 79 | return err |
| 80 | }, |
| 81 | }) |
| 82 | }, |
| 83 | }) |
| 84 | } |
| 85 | |
| 86 | func cmdFileServer(fs caddycmd.Flags) (int, error) { |
| 87 | caddy.TrapSignals() |
| 88 | |
| 89 | domain := fs.String("domain") |
| 90 | root := fs.String("root") |
| 91 | listen := fs.String("listen") |
| 92 | browse := fs.Bool("browse") |
| 93 | templates := fs.Bool("templates") |
| 94 | accessLog := fs.Bool("access-log") |
| 95 | fileLimit := fs.Int("file-limit") |
| 96 | debug := fs.Bool("debug") |
| 97 | revealSymlinks := fs.Bool("reveal-symlinks") |
| 98 | compress := !fs.Bool("no-compress") |
| 99 | precompressed, err := fs.GetStringSlice("precompressed") |
| 100 | if err != nil { |
| 101 | return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid precompressed flag: %v", err) |
| 102 | } |
| 103 | var handlers []json.RawMessage |
| 104 | |
| 105 | if compress { |
| 106 | zstd, err := caddy.GetModule("http.encoders.zstd") |
| 107 | if err != nil { |
| 108 | return caddy.ExitCodeFailedStartup, err |
| 109 | } |
| 110 | |
| 111 | gzip, err := caddy.GetModule("http.encoders.gzip") |
| 112 | if err != nil { |
| 113 | return caddy.ExitCodeFailedStartup, err |
| 114 | } |
| 115 | |
| 116 | handlers = append(handlers, caddyconfig.JSONModuleObject(encode.Encode{ |
| 117 | EncodingsRaw: caddy.ModuleMap{ |
| 118 | "zstd": caddyconfig.JSON(zstd.New(), nil), |
| 119 | "gzip": caddyconfig.JSON(gzip.New(), nil), |
| 120 | }, |
| 121 | Prefer: []string{"zstd", "gzip"}, |
| 122 | }, "handler", "encode", nil)) |
| 123 | } |
| 124 | |
| 125 | if templates { |
| 126 | handler := caddytpl.Templates{FileRoot: root} |
| 127 | handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "templates", nil)) |
| 128 | } |
| 129 | |
| 130 | handler := FileServer{Root: root} |
| 131 | |
| 132 | if len(precompressed) != 0 { |
| 133 | // logic mirrors modules/caddyhttp/fileserver/caddyfile.go case "precompressed" |
| 134 | var order []string |
| 135 | for _, compression := range precompressed { |
| 136 | modID := "http.precompressed." + compression |
| 137 | mod, err := caddy.GetModule(modID) |
| 138 | if err != nil { |
| 139 | return caddy.ExitCodeFailedStartup, fmt.Errorf("getting module named '%s': %v", modID, err) |
| 140 | } |
| 141 | inst := mod.New() |
| 142 | precompress, ok := inst.(encode.Precompressed) |
| 143 | if !ok { |
| 144 | return caddy.ExitCodeFailedStartup, fmt.Errorf("module %s is not a precompressor; is %T", modID, inst) |
| 145 | } |
| 146 | if handler.PrecompressedRaw == nil { |
| 147 | handler.PrecompressedRaw = make(caddy.ModuleMap) |
| 148 | } |
| 149 | handler.PrecompressedRaw[compression] = caddyconfig.JSON(precompress, nil) |
| 150 | order = append(order, compression) |
| 151 | } |
| 152 | handler.PrecompressedOrder = order |
| 153 | } |
| 154 | |
| 155 | if browse { |
| 156 | handler.Browse = &Browse{RevealSymlinks: revealSymlinks, FileLimit: fileLimit} |
| 157 | } |
| 158 | |
| 159 | handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "file_server", nil)) |
| 160 | |
| 161 | route := caddyhttp.Route{HandlersRaw: handlers} |
| 162 | |
| 163 | if domain != "" { |
| 164 | route.MatcherSetsRaw = []caddy.ModuleMap{ |
| 165 | { |
| 166 | "host": caddyconfig.JSON(caddyhttp.MatchHost{domain}, nil), |
| 167 | }, |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | server := &caddyhttp.Server{ |
| 172 | ReadHeaderTimeout: caddy.Duration(10 * time.Second), |
| 173 | IdleTimeout: caddy.Duration(30 * time.Second), |
| 174 | MaxHeaderBytes: 1024 * 10, |
| 175 | Routes: caddyhttp.RouteList{route}, |
| 176 | } |
| 177 | if listen == "" { |
| 178 | if domain == "" { |
| 179 | listen = ":80" |
| 180 | } else { |
| 181 | listen = ":" + strconv.Itoa(certmagic.HTTPSPort) |
| 182 | } |
| 183 | } |
| 184 | server.Listen = []string{listen} |
| 185 | if accessLog { |
| 186 | server.Logs = &caddyhttp.ServerLogConfig{} |
| 187 | } |
| 188 | |
| 189 | httpApp := caddyhttp.App{ |
| 190 | Servers: map[string]*caddyhttp.Server{"static": server}, |
| 191 | } |
| 192 | |
| 193 | var false bool |
| 194 | cfg := &caddy.Config{ |
| 195 | Admin: &caddy.AdminConfig{ |
| 196 | Disabled: true, |
| 197 | Config: &caddy.ConfigSettings{ |
| 198 | Persist: &false, |
| 199 | }, |
| 200 | }, |
| 201 | AppsRaw: caddy.ModuleMap{ |
| 202 | "http": caddyconfig.JSON(httpApp, nil), |
| 203 | }, |
| 204 | } |
| 205 | |
| 206 | if debug { |
| 207 | cfg.Logging = &caddy.Logging{ |
| 208 | Logs: map[string]*caddy.CustomLog{ |
| 209 | "default": { |
| 210 | BaseLog: caddy.BaseLog{Level: zap.DebugLevel.CapitalString()}, |
| 211 | }, |
| 212 | }, |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | err = caddy.Run(cfg) |
| 217 | if err != nil { |
| 218 | return caddy.ExitCodeFailedStartup, err |
| 219 | } |
| 220 | |
| 221 | log.Printf("Caddy serving static files on %s", listen) |
| 222 | |
| 223 | select {} |
| 224 | } |