decred.org/dcrdex@v1.0.5/server/cmd/dexadm/main.go (about) 1 package main 2 3 import ( 4 "context" 5 "crypto/tls" 6 "crypto/x509" 7 "encoding/base64" 8 "fmt" 9 "io" 10 "net/http" 11 "net/url" 12 "os" 13 "os/signal" 14 "path/filepath" 15 16 "decred.org/dcrdex/client/app" 17 "decred.org/dcrdex/dex" 18 "decred.org/dcrdex/server/comms" 19 "github.com/decred/dcrd/dcrutil/v4" 20 "github.com/go-chi/chi/v5" 21 ) 22 23 const ( 24 configFilenameMainnet = "dexadm.conf" 25 configFilenameTestnet = "dexadm_testnet.conf" 26 configFilenameHarness = "dexadm_harness.conf" 27 defaultPort = "31043" 28 ) 29 30 var ( 31 defaultApplicationDirectory = dcrutil.AppDataDir("dexadm", false) 32 ) 33 34 func main() { 35 if err := mainErr(); err != nil { 36 fmt.Fprint(os.Stderr, err, "\n") 37 os.Exit(1) 38 } 39 os.Exit(0) 40 } 41 42 func mainErr() error { 43 ctx, cancel := context.WithCancel(context.Background()) 44 defer cancel() 45 46 comms.UseLogger(dex.StdOutLogger("SRV", dex.LevelInfo)) 47 48 cfg, err := configure() 49 if err != nil { 50 return err 51 } 52 53 killChan := make(chan os.Signal, 1) 54 signal.Notify(killChan, os.Interrupt) 55 go func() { 56 <-killChan 57 fmt.Println("Shutting down...") 58 cancel() 59 }() 60 61 uri, err := url.Parse(cfg.AdminSrvURL) 62 if err != nil { 63 return fmt.Errorf("error parsing adminsrvurl: %w", err) 64 } 65 66 cl := http.DefaultClient 67 68 if len(cfg.AdminSrvCertPath) > 0 { 69 certB, err := os.ReadFile(cfg.AdminSrvCertPath) 70 if err != nil { 71 return fmt.Errorf("error reading certificate file: %w", err) 72 } 73 rootCAs, _ := x509.SystemCertPool() 74 if rootCAs == nil { 75 rootCAs = x509.NewCertPool() 76 } 77 if ok := rootCAs.AppendCertsFromPEM(certB); !ok { 78 return fmt.Errorf("error appending certificate: %w", err) 79 } 80 cl.Transport = &http.Transport{ 81 TLSClientConfig: &tls.Config{ 82 RootCAs: rootCAs, 83 MinVersion: tls.VersionTLS12, 84 ServerName: uri.Hostname(), 85 }, 86 } 87 } 88 89 srv, err := comms.NewServer(&comms.RPCConfig{ 90 ListenAddrs: []string{"127.0.0.1:" + cfg.Port}, 91 NoTLS: true, 92 }) 93 if err != nil { 94 return err 95 } 96 97 mux := srv.Mux() 98 fileServer(mux, "/", "index.html", "text/html") 99 fileServer(mux, "/script.js", "script.js", "text/javascript") 100 101 // Everything else goes to the admin server. 102 mux.NotFound(func(w http.ResponseWriter, r *http.Request) { 103 w.Header().Set("Connection", "close") 104 w.Header().Set("Content-Type", "application/json; charset=utf-8") 105 r.Close = true 106 107 uri := cfg.AdminSrvURL + "/api" + r.URL.Path + "?" + r.URL.RawQuery 108 req, err := http.NewRequest(r.Method, uri, r.Body) 109 if err != nil { 110 http.Error(w, fmt.Sprintf("Error constructing request: %v", err), http.StatusInternalServerError) 111 return 112 } 113 114 // Encode username and password 115 auth := cfg.AdminSrvUsername + ":" + cfg.AdminSrvPassword 116 encodedAuth := base64.StdEncoding.EncodeToString([]byte(auth)) 117 118 // Set the Authorization header 119 req.Header = r.Header 120 req.Header.Add("Authorization", "Basic "+encodedAuth) 121 122 resp, err := cl.Do(req) 123 if err != nil { 124 http.Error(w, fmt.Sprintf("Request error: %v", err), http.StatusInternalServerError) 125 return 126 } 127 defer resp.Body.Close() 128 129 b, _ := io.ReadAll(resp.Body) 130 w.WriteHeader(resp.StatusCode) 131 if len(b) > 0 { 132 _, err = w.Write(append(b, byte('\n'))) 133 if err != nil { 134 fmt.Printf("Write error: %v \n", err) 135 } 136 } 137 }) 138 139 fmt.Println("Open http://127.0.0.1:" + cfg.Port + " in your browser") 140 srv.Run(ctx) 141 142 return nil 143 } 144 145 // writeJSON writes marshals the provided interface and writes the bytes to the 146 func fileServer(r chi.Router, pattern, filePath, contentType string) { 147 // Define a http.HandlerFunc to serve files but not directory indexes. 148 hf := func(w http.ResponseWriter, r *http.Request) { 149 // Ensure the path begins with "/". 150 151 w.Header().Set("Content-Type", contentType) 152 153 // Generate the full file system path and test for existence. 154 fi, err := os.Stat(filePath) 155 if err != nil { 156 http.NotFound(w, r) 157 return 158 } 159 160 // Deny directory listings 161 if fi.IsDir() { 162 http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) 163 return 164 } 165 166 http.ServeFile(w, r, filePath) 167 } 168 169 // Mount the http.HandlerFunc on the pathPrefix. 170 r.Get(pattern, hf) 171 } 172 173 type Config struct { 174 AppData string `long:"appdata" description:"Path to application directory."` 175 ConfigPath string `long:"config" description:"Path to an INI configuration file."` 176 Testnet bool `long:"testnet" description:"use testnet"` 177 Harness bool `long:"harness" description:"use simnet harness"` 178 Port string `long:"port" description:"Web server port"` 179 AdminSrvURL string `long:"adminsrvurl" description:"Administration HTTPS server address (default: 127.0.0.1:6542)."` 180 AdminSrvUsername string `long:"adminsrvuser" description:"Username to user for authentication"` 181 AdminSrvPassword string `long:"adminsrvpass" description:"Admin server password. INSECURE. Do not set unless absolutely necessary."` 182 AdminSrvCertPath string `long:"adminsrvcertpath" description:"TLS certificate for connecting to the admin server"` 183 } 184 185 var DefaultConfig = Config{ 186 AppData: defaultApplicationDirectory, 187 AdminSrvUsername: "u", 188 Port: defaultPort, 189 } 190 191 func configure() (*Config, error) { 192 // Pre-parse the command line options to see if an alternative config file 193 // or the version flag was specified. Override any environment variables 194 // with parsed command line flags. 195 preCfg := DefaultConfig 196 if err := app.ParseCLIConfig(&preCfg); err != nil { 197 return nil, err 198 } 199 200 if preCfg.AppData != defaultApplicationDirectory { 201 preCfg.AppData = dex.CleanAndExpandPath(preCfg.AppData) 202 // If the app directory has been changed, but the config file path hasn't, 203 // reform the config file path with the new directory. 204 } 205 if preCfg.ConfigPath == "" { 206 switch { 207 case preCfg.Harness: 208 preCfg.ConfigPath = configFilenameHarness 209 case preCfg.Testnet: 210 preCfg.ConfigPath = filepath.Join(preCfg.AppData, configFilenameTestnet) 211 default: //mainnet 212 preCfg.ConfigPath = filepath.Join(preCfg.AppData, configFilenameMainnet) 213 } 214 } 215 configPath := dex.CleanAndExpandPath(preCfg.ConfigPath) 216 217 // Load additional config from file. 218 cfg := DefaultConfig 219 if err := app.ParseFileConfig(configPath, &cfg); err != nil { 220 return nil, err 221 } 222 223 if cfg.AdminSrvCertPath == "" { 224 return nil, fmt.Errorf("no adminsrvaddr argument in file or by command-line") 225 } 226 227 if cfg.AdminSrvPassword == "" { 228 return nil, fmt.Errorf("no adminsrvpass argument in file or by command-line") 229 } 230 231 if cfg.AdminSrvCertPath != "" { 232 cfg.AdminSrvCertPath = dex.CleanAndExpandPath(cfg.AdminSrvCertPath) 233 } 234 235 return &cfg, nil 236 }