• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

msoap / shell2http / 4393277156

11 Mar 2023 04:58PM UTC coverage: 66.338% (-0.1%) from 66.446%
4393277156

push

github

GitHub
Added -500 option: for return 500 error if shell exit code != 0 (#94)

13 of 13 new or added lines in 2 files covered. (100.0%)

404 of 609 relevant lines covered (66.34%)

17.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.92
/shell2http.go
1
package main
2

3
import (
4
        "context"
5
        "flag"
6
        "fmt"
7
        "html"
8
        "io"
9
        "io/ioutil"
10
        "log"
11
        "mime/multipart"
12
        "net"
13
        "net/http"
14
        "os"
15
        "os/exec"
16
        "regexp"
17
        "runtime"
18
        "strconv"
19
        "strings"
20
        "time"
21

22
        "github.com/mattn/go-shellwords"
23
        "github.com/msoap/raphanus"
24
        raphanuscommon "github.com/msoap/raphanus/common"
25
)
26

27
var version = "dev"
28

29
const (
30
        // defaultPort - default port for http-server
31
        defaultPort = 8080
32

33
        // shBasicAuthVar - name of env var for basic auth credentials
34
        shBasicAuthVar = "SH_BASIC_AUTH"
35

36
        // defaultShellPOSIX - shell executable by default in POSIX systems
37
        defaultShellPOSIX = "sh"
38

39
        // defaultShellWindows - shell executable by default in Windows
40
        defaultShellWindows = "cmd"
41

42
        // defaultShellPlan9 - shell executable by default in Plan9
43
        defaultShellPlan9 = "rc"
44

45
        maxHTTPCode            = 1000
46
        maxMemoryForUploadFile = 65536
47
)
48

49
// indexTmpl - template for index page
50
const indexTmpl = `<!DOCTYPE html>
51
<!-- Served by shell2http/%s -->
52
<html>
53
<head>
54
    <title>❯ shell2http</title>
55
    <style>
56
    body {
57
        font-family: sans-serif;
58
    }
59
    li {
60
        list-style-type: none;
61
    }
62
    li:before {
63
        content: "❯";
64
        padding-right: 5px;
65
    }
66
    </style>
67
</head>
68
<body>
69
        <h1>shell2http</h1>
70
        <ul>
71
                %s
72
        </ul>
73
        Get from: <a href="https://github.com/msoap/shell2http">github.com/msoap/shell2http</a>
74
</body>
75
</html>
76
`
77

78
// command - one command
79
type command struct {
80
        path       string
81
        cmd        string
82
        httpMethod string
83
        handler    http.HandlerFunc
84
}
85

86
// parsePathAndCommands - get all commands with pathes
87
func parsePathAndCommands(args []string) ([]command, error) {
13✔
88
        var cmdHandlers []command
13✔
89

13✔
90
        if len(args) < 2 || len(args)%2 == 1 {
20✔
91
                return cmdHandlers, fmt.Errorf("requires a pair of path and shell command")
7✔
92
        }
7✔
93

94
        pathRe := regexp.MustCompile(`^(?:([A-Z]+):)?(/\S*)$`)
6✔
95
        uniqPaths := map[string]bool{}
6✔
96

6✔
97
        for i := 0; i < len(args); i += 2 {
19✔
98
                path, cmd := args[i], args[i+1]
13✔
99
                if uniqPaths[path] {
14✔
100
                        return nil, fmt.Errorf("a duplicate path was detected: %q", path)
1✔
101
                }
1✔
102

103
                pathParts := pathRe.FindStringSubmatch(path)
12✔
104
                if len(pathParts) != 3 {
13✔
105
                        return nil, fmt.Errorf("the path %q must begin with the prefix /, and with optional METHOD: prefix", path)
1✔
106
                }
1✔
107
                cmdHandlers = append(cmdHandlers, command{path: pathParts[2], cmd: cmd, httpMethod: pathParts[1]})
11✔
108

11✔
109
                uniqPaths[path] = true
11✔
110
        }
111

112
        return cmdHandlers, nil
4✔
113
}
114

115
// getShellAndParams - get default shell and command
116
func getShellAndParams(cmd string, appConfig Config) (shell string, params []string, err error) {
11✔
117
        shell, params = appConfig.defaultShell, []string{appConfig.defaultShOpt, cmd} // sh -c "cmd"
11✔
118

11✔
119
        // custom shell
11✔
120
        switch {
11✔
121
        case appConfig.shell != appConfig.defaultShell && appConfig.shell != "":
1✔
122
                shell = appConfig.shell
1✔
123
        case appConfig.shell == "":
8✔
124
                cmdLine, err := shellwords.Parse(cmd)
8✔
125
                if err != nil {
9✔
126
                        return shell, params, fmt.Errorf("failed to parse %q: %s", cmd, err)
1✔
127
                }
1✔
128

129
                shell, params = cmdLine[0], cmdLine[1:]
7✔
130
        }
131

132
        return shell, params, nil
10✔
133
}
134

135
// getShellHandler - get handler function for one shell command
136
func getShellHandler(appConfig Config, shell string, params []string, cacheTTL raphanus.DB) func(http.ResponseWriter, *http.Request) {
5✔
137
        reStatusCode := regexp.MustCompile(`^\d+`)
5✔
138

5✔
139
        return func(rw http.ResponseWriter, req *http.Request) {
10✔
140
                shellOut, exitCode, err := execShellCommand(appConfig, shell, params, req, cacheTTL)
5✔
141
                if err != nil {
6✔
142
                        log.Printf("out: %s, exec error: %s", string(shellOut), err)
1✔
143
                }
1✔
144

145
                customStatusCode := 0
5✔
146
                outText := string(shellOut)
5✔
147

5✔
148
                if err != nil && !appConfig.showErrors {
6✔
149
                        outText = fmt.Sprintf("%s\nexec error: %s", string(shellOut), err)
1✔
150
                } else {
5✔
151
                        if appConfig.setCGI {
8✔
152
                                var headers map[string]string
4✔
153
                                outText, headers = parseCGIHeaders(outText)
4✔
154

4✔
155
                                for headerKey, headerValue := range headers {
6✔
156
                                        switch headerKey {
2✔
157
                                        case "Status":
×
158
                                                statusParts := reStatusCode.FindAllString(headerValue, -1)
×
159
                                                if len(statusParts) > 0 {
×
160
                                                        statusCode, err := strconv.Atoi(statusParts[0])
×
161
                                                        if err == nil && statusCode > 0 && statusCode < maxHTTPCode {
×
162
                                                                customStatusCode = statusCode
×
163
                                                                continue
×
164
                                                        }
165
                                                }
166
                                        case "Location":
1✔
167
                                                customStatusCode = http.StatusFound
1✔
168
                                        }
169

170
                                        rw.Header().Set(headerKey, headerValue)
2✔
171
                                }
172
                        }
173
                }
174

175
                rw.Header().Set("X-Shell2http-Exit-Code", strconv.Itoa(exitCode))
5✔
176

5✔
177
                if customStatusCode > 0 {
6✔
178
                        rw.WriteHeader(customStatusCode)
1✔
179
                } else if exitCode > 0 && appConfig.intServerErr {
5✔
180
                        rw.WriteHeader(http.StatusInternalServerError)
×
181
                }
×
182

183
                responseWrite(rw, outText)
5✔
184
        }
185
}
186

187
// execShellCommand - execute shell command, returns bytes out and error
188
func execShellCommand(appConfig Config, shell string, params []string, req *http.Request, cacheTTL raphanus.DB) ([]byte, int, error) {
5✔
189
        if appConfig.cache > 0 {
10✔
190
                if cacheData, err := cacheTTL.GetBytes(req.RequestURI); err != raphanuscommon.ErrKeyNotExists && err != nil {
5✔
191
                        log.Printf("get from cache failed: %s", err)
×
192
                } else if err == nil {
6✔
193
                        // cache hit
1✔
194
                        return cacheData, 0, nil // TODO: save exit code in cache
1✔
195
                }
1✔
196
        }
197

198
        ctx := req.Context()
4✔
199
        if appConfig.timeout > 0 {
4✔
200
                var cancelFn context.CancelFunc
×
201
                ctx, cancelFn = context.WithTimeout(ctx, time.Duration(appConfig.timeout)*time.Second)
×
202
                defer cancelFn()
×
203
        }
×
204
        osExecCommand := exec.CommandContext(ctx, shell, params...) // #nosec
4✔
205

4✔
206
        proxySystemEnv(osExecCommand, appConfig)
4✔
207

4✔
208
        finalizer := func() {}
8✔
209
        if appConfig.setForm {
4✔
210
                var err error
×
211
                if finalizer, err = getForm(osExecCommand, req, appConfig.formCheckRe); err != nil {
×
212
                        log.Printf("parse form failed: %s", err)
×
213
                }
×
214
        }
215

216
        var (
4✔
217
                waitPipeWrite bool
4✔
218
                pipeErrCh     = make(chan error)
4✔
219
                shellOut      []byte
4✔
220
                err           error
4✔
221
        )
4✔
222

4✔
223
        if appConfig.setCGI {
8✔
224
                setCGIEnv(osExecCommand, req, appConfig)
4✔
225

4✔
226
                // get request body data data to stdin of script (if not parse form vars above)
4✔
227
                if (req.Method == "POST" || req.Method == "PUT" || req.Method == "PATCH") && !appConfig.setForm {
5✔
228
                        if stdin, pipeErr := osExecCommand.StdinPipe(); pipeErr != nil {
1✔
229
                                log.Println("write request body data to shell failed:", pipeErr)
×
230
                        } else {
1✔
231
                                waitPipeWrite = true
1✔
232
                                go func() {
2✔
233
                                        if _, pipeErr := io.Copy(stdin, req.Body); pipeErr != nil {
1✔
234
                                                pipeErrCh <- pipeErr
×
235
                                                return
×
236
                                        }
×
237
                                        pipeErrCh <- stdin.Close()
1✔
238
                                }()
239
                        }
240
                }
241
        }
242

243
        if appConfig.includeStderr {
4✔
244
                shellOut, err = osExecCommand.CombinedOutput()
×
245
        } else {
4✔
246
                osExecCommand.Stderr = os.Stderr
4✔
247
                shellOut, err = osExecCommand.Output()
4✔
248
        }
4✔
249

250
        if waitPipeWrite {
5✔
251
                if pipeErr := <-pipeErrCh; pipeErr != nil {
1✔
252
                        log.Println("write request body data to shell failed:", pipeErr)
×
253
                }
×
254
        }
255

256
        finalizer()
4✔
257

4✔
258
        if appConfig.cache > 0 {
8✔
259
                if cacheErr := cacheTTL.SetBytes(req.RequestURI, shellOut, appConfig.cache); cacheErr != nil {
4✔
260
                        log.Printf("set to cache failed: %s", cacheErr)
×
261
                }
×
262
        }
263

264
        exitCode := osExecCommand.ProcessState.ExitCode()
4✔
265

4✔
266
        return shellOut, exitCode, err
4✔
267
}
268

269
// setupHandlers - setup http handlers
270
func setupHandlers(cmdHandlers []command, appConfig Config, cacheTTL raphanus.DB) ([]command, error) {
1✔
271
        resultHandlers := []command{}
1✔
272
        indexLiHTML := ""
1✔
273
        existsRootPath := false
1✔
274

1✔
275
        // map[path][http-method]handler
1✔
276
        groupedCmd := map[string]map[string]http.HandlerFunc{}
1✔
277
        cmdsForLog := map[string][]string{}
1✔
278

1✔
279
        for _, row := range cmdHandlers {
6✔
280
                path, cmd := row.path, row.cmd
5✔
281
                shell, params, err := getShellAndParams(cmd, appConfig)
5✔
282
                if err != nil {
5✔
283
                        return nil, err
×
284
                }
×
285

286
                existsRootPath = existsRootPath || path == "/"
5✔
287

5✔
288
                methodDesc := ""
5✔
289
                if row.httpMethod != "" {
8✔
290
                        methodDesc = row.httpMethod + ": "
3✔
291
                }
3✔
292
                indexLiHTML += fmt.Sprintf(`<li><a href=".%s">%s%s</a> <span style="color: #888">- %s<span></li>`, path, methodDesc, path, html.EscapeString(cmd))
5✔
293
                cmdsForLog[path] = append(cmdsForLog[path], cmd)
5✔
294

5✔
295
                handler := mwMethodOnly(getShellHandler(appConfig, shell, params, cacheTTL), row.httpMethod)
5✔
296
                if _, ok := groupedCmd[path]; !ok {
10✔
297
                        groupedCmd[path] = map[string]http.HandlerFunc{}
5✔
298
                }
5✔
299
                groupedCmd[path][row.httpMethod] = handler
5✔
300
        }
301

302
        for path, cmds := range groupedCmd {
6✔
303
                handler, err := mwMultiMethod(cmds)
5✔
304
                if err != nil {
5✔
305
                        return nil, err
×
306
                }
×
307
                resultHandlers = append(resultHandlers, command{
5✔
308
                        path:    path,
5✔
309
                        handler: handler,
5✔
310
                        cmd:     strings.Join(cmdsForLog[path], "; "),
5✔
311
                })
5✔
312
        }
313

314
        // --------------
315
        if appConfig.addExit {
2✔
316
                resultHandlers = append(resultHandlers, command{
1✔
317
                        path: "/exit",
1✔
318
                        cmd:  "/exit",
1✔
319
                        handler: func(rw http.ResponseWriter, _ *http.Request) {
1✔
320
                                responseWrite(rw, "Bye...")
×
321
                                go os.Exit(0)
×
322
                        },
×
323
                })
324

325
                indexLiHTML += fmt.Sprintf(`<li><a href=".%s">%s</a></li>`, "/exit", "/exit")
1✔
326
        }
327

328
        // --------------
329
        if !appConfig.noIndex && !existsRootPath {
2✔
330
                indexHTML := fmt.Sprintf(indexTmpl, version, indexLiHTML)
1✔
331
                resultHandlers = append(resultHandlers, command{
1✔
332
                        path: "/",
1✔
333
                        cmd:  "index page",
1✔
334
                        handler: func(rw http.ResponseWriter, req *http.Request) {
4✔
335
                                if req.URL.Path != "/" {
4✔
336
                                        log.Printf("%s - 404", req.URL.Path)
1✔
337
                                        http.NotFound(rw, req)
1✔
338
                                        return
1✔
339
                                }
1✔
340

341
                                responseWrite(rw, indexHTML)
2✔
342
                        },
343
                })
344
        }
345

346
        return resultHandlers, nil
1✔
347
}
348

349
// responseWrite - write text to response
350
func responseWrite(rw io.Writer, text string) {
7✔
351
        if _, err := io.WriteString(rw, text); err != nil {
7✔
352
                log.Printf("print string failed: %s", err)
×
353
        }
×
354
}
355

356
// setCGIEnv - set some CGI variables
357
func setCGIEnv(cmd *exec.Cmd, req *http.Request, appConfig Config) {
4✔
358
        // set HTTP_* variables
4✔
359
        for headerName, headerValue := range req.Header {
17✔
360
                envName := strings.ToUpper(strings.Replace(headerName, "-", "_", -1))
13✔
361
                if envName == "PROXY" {
13✔
362
                        continue
×
363
                }
364
                cmd.Env = append(cmd.Env, fmt.Sprintf("HTTP_%s=%s", envName, headerValue[0]))
13✔
365
        }
366

367
        remoteHost, remotePort, err := net.SplitHostPort(req.RemoteAddr)
4✔
368
        if err != nil {
4✔
369
                log.Printf("failed to parse remote address %s: %s", req.RemoteAddr, err)
×
370
        }
×
371
        CGIVars := [...]struct {
4✔
372
                cgiName, value string
4✔
373
        }{
4✔
374
                {"PATH_INFO", req.URL.Path},
4✔
375
                {"QUERY_STRING", req.URL.RawQuery},
4✔
376
                {"REMOTE_ADDR", remoteHost},
4✔
377
                {"REMOTE_PORT", remotePort},
4✔
378
                {"REQUEST_METHOD", req.Method},
4✔
379
                {"REQUEST_URI", req.RequestURI},
4✔
380
                {"SCRIPT_NAME", req.URL.Path},
4✔
381
                {"SERVER_NAME", appConfig.host},
4✔
382
                {"SERVER_PORT", strconv.Itoa(appConfig.port)},
4✔
383
                {"SERVER_PROTOCOL", req.Proto},
4✔
384
                {"SERVER_SOFTWARE", "shell2http"},
4✔
385
        }
4✔
386

4✔
387
        for _, row := range CGIVars {
48✔
388
                cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", row.cgiName, row.value))
44✔
389
        }
44✔
390
}
391

392
/*
393
        parse headers from script output:
394

395
Header-name1: value1\n
396
Header-name2: value2\n
397
\n
398
text
399
*/
400
func parseCGIHeaders(shellOut string) (string, map[string]string) {
15✔
401
        parts := regexp.MustCompile(`\r?\n\r?\n`).Split(shellOut, 2)
15✔
402
        if len(parts) == 2 {
27✔
403

12✔
404
                headerRe := regexp.MustCompile(`^([^:\s]+):\s*(\S.*)$`)
12✔
405
                headerLines := regexp.MustCompile(`\r?\n`).Split(parts[0], -1)
12✔
406
                headersMap := map[string]string{}
12✔
407

12✔
408
                for _, headerLine := range headerLines {
27✔
409
                        headerParts := headerRe.FindStringSubmatch(headerLine)
15✔
410
                        if len(headerParts) == 3 {
26✔
411
                                headersMap[headerParts[1]] = headerParts[2]
11✔
412
                        } else {
15✔
413
                                // headers is not valid, return all text
4✔
414
                                return shellOut, nil
4✔
415
                        }
4✔
416
                }
417

418
                return parts[1], headersMap
8✔
419
        }
420

421
        // headers don't found, return all text
422
        return shellOut, nil
3✔
423
}
424

425
// getForm - parse form into environment vars, also handle uploaded files
426
func getForm(cmd *exec.Cmd, req *http.Request, checkFormRe *regexp.Regexp) (func(), error) {
×
427
        tempDir := ""
×
428
        safeFileNameRe := regexp.MustCompile(`[^\.\w\-]+`)
×
429
        finalizer := func() {
×
430
                if tempDir != "" {
×
431
                        if err := os.RemoveAll(tempDir); err != nil {
×
432
                                log.Println(err)
×
433
                        }
×
434
                }
435
        }
436

437
        if err := req.ParseForm(); err != nil {
×
438
                return finalizer, err
×
439
        }
×
440

441
        if isMultipartFormData(req.Header) {
×
442
                if err := req.ParseMultipartForm(maxMemoryForUploadFile); err != nil {
×
443
                        return finalizer, err
×
444
                }
×
445
        }
446

447
        for key, values := range req.Form {
×
448
                if checkFormRe != nil {
×
449
                        checkedValues := []string{}
×
450
                        for _, v := range values {
×
451
                                if checkFormRe.MatchString(v) {
×
452
                                        checkedValues = append(checkedValues, v)
×
453
                                }
×
454
                        }
455
                        values = checkedValues
×
456
                }
457
                if len(values) == 0 {
×
458
                        continue
×
459
                }
460

461
                value := strings.Join(values, ",")
×
462
                cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", "v_"+key, value))
×
463
        }
464

465
        // handle uploaded files, save all to temporary files and set variables filename_XXX, filepath_XXX
466
        if req.MultipartForm != nil {
×
467
                for key, value := range req.MultipartForm.File {
×
468
                        if len(value) == 1 {
×
469
                                var (
×
470
                                        uplFile     multipart.File
×
471
                                        outFile     *os.File
×
472
                                        err         error
×
473
                                        reqFileName = value[0].Filename
×
474
                                )
×
475

×
476
                                errCreate := errChain(func() error {
×
477
                                        uplFile, err = value[0].Open()
×
478
                                        return err
×
479
                                }, func() error {
×
480
                                        tempDir, err = ioutil.TempDir("", "shell2http_")
×
481
                                        return err
×
482
                                }, func() error {
×
483
                                        prefix := safeFileNameRe.ReplaceAllString(reqFileName, "")
×
484
                                        outFile, err = ioutil.TempFile(tempDir, prefix+"_")
×
485
                                        return err
×
486
                                }, func() error {
×
487
                                        _, err = io.Copy(outFile, uplFile)
×
488
                                        return err
×
489
                                })
×
490

491
                                errClose := errChainAll(func() error {
×
492
                                        if uplFile != nil {
×
493
                                                return uplFile.Close()
×
494
                                        }
×
495
                                        return nil
×
496
                                }, func() error {
×
497
                                        if outFile != nil {
×
498
                                                return outFile.Close()
×
499
                                        }
×
500
                                        return nil
×
501
                                })
502
                                if errClose != nil {
×
503
                                        return finalizer, errClose
×
504
                                }
×
505

506
                                if errCreate != nil {
×
507
                                        return finalizer, errCreate
×
508
                                }
×
509

510
                                cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", "filepath_"+key, outFile.Name()))
×
511
                                cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", "filename_"+key, reqFileName))
×
512
                        }
513
                }
514
        }
515

516
        return finalizer, nil
×
517
}
518

519
// isMultipartFormData - check header for multipart/form-data
520
func isMultipartFormData(headers http.Header) bool {
×
521
        if contentType, ok := headers["Content-Type"]; ok && len(contentType) == 1 && strings.HasPrefix(contentType[0], "multipart/form-data; ") {
×
522
                return true
×
523
        }
×
524

525
        return false
×
526
}
527

528
// proxySystemEnv - proxy some system vars
529
func proxySystemEnv(cmd *exec.Cmd, appConfig Config) {
4✔
530
        varsNames := []string{"PATH", "HOME", "LANG", "USER", "TMPDIR"}
4✔
531

4✔
532
        if runtime.GOOS == "windows" {
4✔
533
                varsNames = append(varsNames, "USERNAME", "USERPROFILE", "HOMEDRIVE", "HOMEPATH", "TEMP", "TMP", "PATHEXT", "COMSPEC", "OS")
×
534
        }
×
535

536
        if appConfig.exportVars != "" {
8✔
537
                varsNames = append(varsNames, strings.Split(appConfig.exportVars, ",")...)
4✔
538
        }
4✔
539

540
        for _, envRaw := range os.Environ() {
444✔
541
                env := strings.SplitN(envRaw, "=", 2)
440✔
542
                if env[0] != shBasicAuthVar {
880✔
543
                        if appConfig.exportAllVars {
440✔
544
                                cmd.Env = append(cmd.Env, envRaw)
×
545
                        } else {
440✔
546
                                for _, envVarName := range varsNames {
3,080✔
547
                                        if strings.ToUpper(env[0]) == envVarName {
2,660✔
548
                                                cmd.Env = append(cmd.Env, envRaw)
20✔
549
                                        }
20✔
550
                                }
551
                        }
552
                }
553
        }
554
}
555

556
// errChain - handle errors on few functions
557
func errChain(chainFuncs ...func() error) error {
6✔
558
        for _, fn := range chainFuncs {
13✔
559
                if err := fn(); err != nil {
10✔
560
                        return err
3✔
561
                }
3✔
562
        }
563

564
        return nil
3✔
565
}
566

567
// errChainAll - handle errors on few functions, exec all func and returns the first error
568
func errChainAll(chainFuncs ...func() error) error {
6✔
569
        var resErr error
6✔
570
        for _, fn := range chainFuncs {
14✔
571
                if err := fn(); err != nil {
11✔
572
                        resErr = err
3✔
573
                }
3✔
574
        }
575

576
        return resErr
6✔
577
}
578

579
func main() {
1✔
580
        appConfig, err := getConfig()
1✔
581
        if err != nil {
1✔
582
                log.Fatal(err)
×
583
        }
×
584

585
        cmdHandlers, err := parsePathAndCommands(flag.Args())
1✔
586
        if err != nil {
1✔
587
                log.Fatalf("failed to parse arguments: %s", err)
×
588
        }
×
589

590
        var cacheTTL raphanus.DB
1✔
591
        if appConfig.cache > 0 {
2✔
592
                cacheTTL = raphanus.New()
1✔
593
        }
1✔
594

595
        cmdHandlers, err = setupHandlers(cmdHandlers, *appConfig, cacheTTL)
1✔
596
        if err != nil {
1✔
597
                log.Fatal(err)
×
598
        }
×
599
        for _, handler := range cmdHandlers {
8✔
600
                handlerFunc := handler.handler
7✔
601
                if len(appConfig.auth.users) > 0 {
7✔
602
                        handlerFunc = mwBasicAuth(handlerFunc, appConfig.auth)
×
603
                }
×
604
                if appConfig.oneThread {
14✔
605
                        handlerFunc = mwOneThread(handlerFunc)
7✔
606
                }
7✔
607
                handlerFunc = mwLogging(mwCommonHeaders(handlerFunc))
7✔
608

7✔
609
                http.HandleFunc(handler.path, handlerFunc)
7✔
610
                log.Printf("register: %s (%s)\n", handler.path, handler.cmd)
7✔
611
        }
612

613
        listener, err := net.Listen("tcp", net.JoinHostPort(appConfig.host, strconv.Itoa(appConfig.port)))
1✔
614
        if err != nil {
1✔
615
                log.Fatal(err)
×
616
        }
×
617

618
        log.Printf("listen %s\n", appConfig.readableURL(listener.Addr()))
1✔
619

1✔
620
        if len(appConfig.cert) > 0 && len(appConfig.key) > 0 {
1✔
621
                log.Fatal(http.ServeTLS(listener, nil, appConfig.cert, appConfig.key))
×
622
        } else {
1✔
623
                log.Fatal(http.Serve(listener, nil))
1✔
624
        }
1✔
625
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc