@@ -9,13 +9,16 @@ import (
99 "fmt"
1010 "io"
1111 "log/slog"
12+ "net"
1213 "net/http"
1314 "os"
1415 "os/signal"
1516 "sort"
17+ "time"
1618
1719 "github.com/redstone-md/Doppel/internal/ca"
1820 "github.com/redstone-md/Doppel/internal/config"
21+ "github.com/redstone-md/Doppel/internal/launch"
1922 "github.com/redstone-md/Doppel/internal/mitm"
2023 "github.com/redstone-md/Doppel/internal/profile"
2124 "github.com/redstone-md/Doppel/internal/proxy"
@@ -38,6 +41,8 @@ func main() {
3841 err = cmdInit (os .Args [2 :])
3942 case "run" :
4043 err = cmdRun (os .Args [2 :])
44+ case "launch" :
45+ err = cmdLaunch (os .Args [2 :])
4146 case "profiles" :
4247 err = cmdProfiles (os .Args [2 :])
4348 case "ca" :
@@ -152,6 +157,104 @@ func cmdRun(args []string) error {
152157 return server .ListenAndServe (ctx )
153158}
154159
160+ // cmdLaunch starts the proxy, launches a child application configured to use it,
161+ // and stops the proxy when the child exits.
162+ func cmdLaunch (args []string ) error {
163+ cfg , err := config .Default ()
164+ if err != nil {
165+ return err
166+ }
167+ fs := flag .NewFlagSet ("launch" , flag .ExitOnError )
168+ addr := fs .String ("addr" , cfg .Addr , "proxy listen address" )
169+ profileName := fs .String ("profile" , cfg .Profile , "identity profile to emulate" )
170+ dataDir := fs .String ("data" , cfg .DataDir , "data directory" )
171+ verbose := fs .Bool ("v" , false , "verbose (debug) logging" )
172+ insecure := fs .Bool ("insecure" , false , "skip upstream certificate verification (debugging only)" )
173+ includeEnv := fs .Bool ("env" , true , "set HTTPS proxy and CA environment variables for the child" )
174+ electron := fs .Bool ("electron" , false , "append Chromium/Electron proxy command-line switches" )
175+ allSchemes := fs .Bool ("all-schemes" , false , "with -electron, proxy every Chromium URL scheme instead of HTTPS only" )
176+ bypass := fs .String ("bypass" , "<local>" , "Chromium proxy bypass list used with -electron" )
177+ if err := fs .Parse (args ); err != nil {
178+ return err
179+ }
180+ argv := fs .Args ()
181+ if len (argv ) == 0 {
182+ return fmt .Errorf ("usage: doppel launch [flags] -- <command> [args...]" )
183+ }
184+ cfg .DataDir , cfg .Addr , cfg .Profile = * dataDir , * addr , * profileName
185+
186+ logger := newLogger (* verbose )
187+
188+ if ! cfg .CAExists () {
189+ return fmt .Errorf ("no CA found in %s; run 'doppel init' first" , cfg .DataDir )
190+ }
191+ authority , err := ca .Load (cfg .CACertPath (), cfg .CAKeyPath ())
192+ if err != nil {
193+ return err
194+ }
195+
196+ selected , err := loadProfile (cfg , cfg .Profile )
197+ if err != nil {
198+ return err
199+ }
200+
201+ transport := & upstream.RoundTripper {
202+ Dialer : & upstream.Dialer {SkipVerify : * insecure },
203+ Profile : selected ,
204+ }
205+ defer transport .Close ()
206+
207+ ctx , stop := signal .NotifyContext (context .Background (), os .Interrupt )
208+ defer stop ()
209+
210+ ln , err := net .Listen ("tcp" , cfg .Addr )
211+ if err != nil {
212+ return fmt .Errorf ("listen on %s: %w" , cfg .Addr , err )
213+ }
214+
215+ server := & proxy.Server {
216+ Addr : cfg .Addr ,
217+ Logger : logger ,
218+ Interceptor : & mitm.Interceptor {
219+ CA : authority ,
220+ Profile : selected ,
221+ Transport : transport ,
222+ Logger : logger ,
223+ },
224+ }
225+
226+ serveErr := make (chan error , 1 )
227+ go func () { serveErr <- server .Serve (ctx , ln ) }()
228+
229+ proxyAddr := ln .Addr ().String ()
230+ cmd , err := launch .Command (ctx , launch.Options {
231+ ProxyAddr : proxyAddr ,
232+ CACertPath : cfg .CACertPath (),
233+ IncludeEnv : * includeEnv ,
234+ IncludeChromium : * electron ,
235+ ProxyAllSchemes : * allSchemes ,
236+ BypassList : * bypass ,
237+ }, argv )
238+ if err != nil {
239+ stop ()
240+ return err
241+ }
242+
243+ logger .Info ("launching app" , "profile" , selected .Name , "proxy" , proxyAddr , "command" , argv [0 ])
244+ runErr := cmd .Run ()
245+ stop ()
246+
247+ select {
248+ case err := <- serveErr :
249+ if runErr == nil && err != nil {
250+ return err
251+ }
252+ case <- time .After (5 * time .Second ):
253+ logger .Warn ("proxy shutdown timed out; exiting with child status" )
254+ }
255+ return runErr
256+ }
257+
155258// cmdProfiles lists every available identity profile.
156259func cmdProfiles (args []string ) error {
157260 cfg , err := config .Default ()
@@ -320,6 +423,7 @@ Usage:
320423Commands:
321424 init Generate the local CA and print setup instructions
322425 run Start the proxy
426+ launch Start the proxy and run an application through it
323427 profiles List available identity profiles
324428 ca Show or export the local CA certificate
325429 verify Check the emulated TLS fingerprint against a remote service
0 commit comments