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

msoap / shell2http / 4153120223

11 Feb 2023 08:57PM UTC coverage: 66.446% (-1.4%) from 67.857%
4153120223

push

github

GitHub
Added -form-check option for restrict form values (#91)

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

402 of 605 relevant lines covered (66.45%)

17.13 hits per line

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

68.18
/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
                rw.Header().Set("X-Shell2http-Exit-Code", strconv.Itoa(exitCode))
5✔
146

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

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

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

174
                                if customStatusCode > 0 {
5✔
175
                                        rw.WriteHeader(customStatusCode)
1✔
176
                                }
1✔
177
                        }
178

179
                        responseWrite(rw, outText)
4✔
180
                }
181
        }
182
}
183

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

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

4✔
203
        proxySystemEnv(osExecCommand, appConfig)
4✔
204

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

213
        var (
4✔
214
                waitPipeWrite bool
4✔
215
                pipeErrCh     = make(chan error)
4✔
216
                shellOut      []byte
4✔
217
                err           error
4✔
218
        )
4✔
219

4✔
220
        if appConfig.setCGI {
8✔
221
                setCGIEnv(osExecCommand, req, appConfig)
4✔
222

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

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

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

253
        finalizer()
4✔
254

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

261
        exitCode := osExecCommand.ProcessState.ExitCode()
4✔
262

4✔
263
        return shellOut, exitCode, err
4✔
264
}
265

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

1✔
272
        // map[path][http-method]handler
1✔
273
        groupedCmd := map[string]map[string]http.HandlerFunc{}
1✔
274
        cmdsForLog := map[string][]string{}
1✔
275

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

283
                existsRootPath = existsRootPath || path == "/"
5✔
284

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

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

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

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

322
                indexLiHTML += fmt.Sprintf(`<li><a href=".%s">%s</a></li>`, "/exit", "/exit")
1✔
323
        }
324

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

338
                                responseWrite(rw, indexHTML)
2✔
339
                        },
340
                })
341
        }
342

343
        return resultHandlers, nil
1✔
344
}
345

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

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

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

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

389
/*
390
        parse headers from script output:
391

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

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

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

415
                return parts[1], headersMap
8✔
416
        }
417

418
        // headers don't found, return all text
419
        return shellOut, map[string]string{}
3✔
420
}
421

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

434
        if err := req.ParseForm(); err != nil {
×
435
                return finalizer, err
×
436
        }
×
437

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

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

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

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

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

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

503
                                if errCreate != nil {
×
504
                                        return finalizer, errCreate
×
505
                                }
×
506

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

513
        return finalizer, nil
×
514
}
515

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

522
        return false
×
523
}
524

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

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

533
        if appConfig.exportVars != "" {
8✔
534
                varsNames = append(varsNames, strings.Split(appConfig.exportVars, ",")...)
4✔
535
        }
4✔
536

537
        for _, envRaw := range os.Environ() {
432✔
538
                env := strings.SplitN(envRaw, "=", 2)
428✔
539
                if env[0] != shBasicAuthVar {
856✔
540
                        if appConfig.exportAllVars {
428✔
541
                                cmd.Env = append(cmd.Env, envRaw)
×
542
                        } else {
428✔
543
                                for _, envVarName := range varsNames {
2,996✔
544
                                        if strings.ToUpper(env[0]) == envVarName {
2,588✔
545
                                                cmd.Env = append(cmd.Env, envRaw)
20✔
546
                                        }
20✔
547
                                }
548
                        }
549
                }
550
        }
551
}
552

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

561
        return nil
3✔
562
}
563

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

573
        return resErr
6✔
574
}
575

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

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

587
        var cacheTTL raphanus.DB
1✔
588
        if appConfig.cache > 0 {
2✔
589
                cacheTTL = raphanus.New()
1✔
590
        }
1✔
591

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

7✔
606
                http.HandleFunc(handler.path, handlerFunc)
7✔
607
                log.Printf("register: %s (%s)\n", handler.path, handler.cmd)
7✔
608
        }
609

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

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

1✔
617
        if len(appConfig.cert) > 0 && len(appConfig.key) > 0 {
1✔
618
                log.Fatal(http.ServeTLS(listener, nil, appConfig.cert, appConfig.key))
×
619
        } else {
1✔
620
                log.Fatal(http.Serve(listener, nil))
1✔
621
        }
1✔
622
}
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