Project

General

Profile

Bug #6263 ยป coverage-services_arv-git-httpd.html

Tom Clegg, 08/27/2015 07:49 PM

 
1

    
2
<!DOCTYPE html>
3
<html>
4
	<head>
5
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
6
		<style>
7
			body {
8
				background: black;
9
				color: rgb(80, 80, 80);
10
			}
11
			body, pre, #legend span {
12
				font-family: Menlo, monospace;
13
				font-weight: bold;
14
			}
15
			#topbar {
16
				background: black;
17
				position: fixed;
18
				top: 0; left: 0; right: 0;
19
				height: 42px;
20
				border-bottom: 1px solid rgb(80, 80, 80);
21
			}
22
			#content {
23
				margin-top: 50px;
24
			}
25
			#nav, #legend {
26
				float: left;
27
				margin-left: 10px;
28
			}
29
			#legend {
30
				margin-top: 12px;
31
			}
32
			#nav {
33
				margin-top: 10px;
34
			}
35
			#legend span {
36
				margin: 0 5px;
37
			}
38
			.cov0 { color: rgb(192, 0, 0) }
39
.cov1 { color: rgb(128, 128, 128) }
40
.cov2 { color: rgb(116, 140, 131) }
41
.cov3 { color: rgb(104, 152, 134) }
42
.cov4 { color: rgb(92, 164, 137) }
43
.cov5 { color: rgb(80, 176, 140) }
44
.cov6 { color: rgb(68, 188, 143) }
45
.cov7 { color: rgb(56, 200, 146) }
46
.cov8 { color: rgb(44, 212, 149) }
47
.cov9 { color: rgb(32, 224, 152) }
48
.cov10 { color: rgb(20, 236, 155) }
49

    
50
		</style>
51
	</head>
52
	<body>
53
		<div id="topbar">
54
			<div id="nav">
55
				<select id="files">
56
				
57
				<option value="file0">git.curoverse.com/arvados.git/services/arv-git-httpd/auth_handler.go (89.0%)</option>
58
				
59
				<option value="file1">git.curoverse.com/arvados.git/services/arv-git-httpd/git_handler.go (100.0%)</option>
60
				
61
				<option value="file2">git.curoverse.com/arvados.git/services/arv-git-httpd/main.go (43.8%)</option>
62
				
63
				<option value="file3">git.curoverse.com/arvados.git/services/arv-git-httpd/server.go (100.0%)</option>
64
				
65
				</select>
66
			</div>
67
			<div id="legend">
68
				<span>not tracked</span>
69
			
70
				<span class="cov0">no coverage</span>
71
				<span class="cov1">low coverage</span>
72
				<span class="cov2">*</span>
73
				<span class="cov3">*</span>
74
				<span class="cov4">*</span>
75
				<span class="cov5">*</span>
76
				<span class="cov6">*</span>
77
				<span class="cov7">*</span>
78
				<span class="cov8">*</span>
79
				<span class="cov9">*</span>
80
				<span class="cov10">high coverage</span>
81
			
82
			</div>
83
		</div>
84
		<div id="content">
85
		
86
		<pre class="file" id="file0" >package main
87

    
88
import (
89
        "log"
90
        "net/http"
91
        "os"
92
        "strings"
93
        "time"
94

    
95
        "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
96
        "git.curoverse.com/arvados.git/sdk/go/auth"
97
        "git.curoverse.com/arvados.git/sdk/go/httpserver"
98
)
99

    
100
var clientPool = arvadosclient.MakeClientPool()
101

    
102
type authHandler struct {
103
        handler http.Handler
104
}
105

    
106
func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) <span class="cov10" title="74">{
107
        var statusCode int
108
        var statusText string
109
        var apiToken string
110
        var repoName string
111
        var validApiToken bool
112

    
113
        w := httpserver.WrapResponseWriter(wOrig)
114

    
115
        defer func() </span><span class="cov10" title="74">{
116
                if w.WroteStatus() == 0 </span><span class="cov9" title="61">{
117
                        // Nobody has called WriteHeader yet: that
118
                        // must be our job.
119
                        w.WriteHeader(statusCode)
120
                        w.Write([]byte(statusText))
121
                }</span>
122

    
123
                // If the given password is a valid token, log the first 10 characters of the token.
124
                // Otherwise: log the string &lt;invalid&gt; if a password is given, else an empty string.
125
                <span class="cov10" title="74">passwordToLog := ""
126
                if !validApiToken </span><span class="cov9" title="52">{
127
                        if len(apiToken) &gt; 0 </span><span class="cov6" title="14">{
128
                                passwordToLog = "&lt;invalid&gt;"
129
                        }</span>
130
                }<span class="cov7" title="22"> else {
131
                        passwordToLog = apiToken[0:10]
132
                }</span>
133

    
134
                <span class="cov10" title="74">httpserver.Log(r.RemoteAddr, passwordToLog, w.WroteStatus(), statusText, repoName, r.Method, r.URL.Path)</span>
135
        }()
136

    
137
        <span class="cov10" title="74">creds := auth.NewCredentialsFromHTTPRequest(r)
138
        if len(creds.Tokens) == 0 </span><span class="cov8" title="38">{
139
                statusCode, statusText = http.StatusUnauthorized, "no credentials provided"
140
                w.Header().Add("WWW-Authenticate", "Basic realm=\"git\"")
141
                return
142
        }</span>
143
        <span class="cov8" title="36">apiToken = creds.Tokens[0]
144

    
145
        // Access to paths "/foo/bar.git/*" and "/foo/bar/.git/*" are
146
        // protected by the permissions on the repository named
147
        // "foo/bar".
148
        pathParts := strings.SplitN(r.URL.Path[1:], ".git/", 2)
149
        if len(pathParts) != 2 </span><span class="cov2" title="2">{
150
                statusCode, statusText = http.StatusBadRequest, "bad request"
151
                return
152
        }</span>
153
        <span class="cov8" title="34">repoName = pathParts[0]
154
        repoName = strings.TrimRight(repoName, "/")
155

    
156
        arv := clientPool.Get()
157
        if arv == nil </span><span class="cov0" title="0">{
158
                statusCode, statusText = http.StatusInternalServerError, "connection pool failed: "+clientPool.Err().Error()
159
                return
160
        }</span>
161
        <span class="cov8" title="34">defer clientPool.Put(arv)
162

    
163
        // Ask API server whether the repository is readable using
164
        // this token (by trying to read it!)
165
        arv.ApiToken = apiToken
166
        reposFound := arvadosclient.Dict{}
167
        if err := arv.List("repositories", arvadosclient.Dict{
168
                "filters": [][]string{{"name", "=", repoName}},
169
        }, &amp;reposFound); err != nil </span><span class="cov6" title="12">{
170
                statusCode, statusText = http.StatusInternalServerError, err.Error()
171
                return
172
        }</span>
173
        <span class="cov7" title="22">validApiToken = true
174
        if avail, ok := reposFound["items_available"].(float64); !ok </span><span class="cov0" title="0">{
175
                statusCode, statusText = http.StatusInternalServerError, "bad list response from API"
176
                return
177
        }</span><span class="cov7" title="22"> else if avail &lt; 1 </span><span class="cov4" title="6">{
178
                statusCode, statusText = http.StatusNotFound, "not found"
179
                return
180
        }</span><span class="cov6" title="16"> else if avail &gt; 1 </span><span class="cov0" title="0">{
181
                statusCode, statusText = http.StatusInternalServerError, "name collision"
182
                return
183
        }</span>
184

    
185
        <span class="cov6" title="16">repoUUID := reposFound["items"].([]interface{})[0].(map[string]interface{})["uuid"].(string)
186

    
187
        isWrite := strings.HasSuffix(r.URL.Path, "/git-receive-pack")
188
        if !isWrite </span><span class="cov6" title="14">{
189
                statusText = "read"
190
        }</span><span class="cov2" title="2"> else {
191
                err := arv.Update("repositories", repoUUID, arvadosclient.Dict{
192
                        "repository": arvadosclient.Dict{
193
                                "modified_at": time.Now().String(),
194
                        },
195
                }, &amp;arvadosclient.Dict{})
196
                if err != nil </span><span class="cov1" title="1">{
197
                        statusCode, statusText = http.StatusForbidden, err.Error()
198
                        return
199
                }</span>
200
                <span class="cov1" title="1">statusText = "write"</span>
201
        }
202

    
203
        // Regardless of whether the client asked for "/foo.git" or
204
        // "/foo/.git", we choose whichever variant exists in our repo
205
        // root, and we try {uuid}.git and {uuid}/.git first. If none
206
        // of these exist, we 404 even though the API told us the repo
207
        // _should_ exist (presumably this means the repo was just
208
        // created, and gitolite sync hasn't run yet).
209
        <span class="cov6" title="15">rewrittenPath := ""
210
        tryDirs := []string{
211
                "/" + repoUUID + ".git",
212
                "/" + repoUUID + "/.git",
213
                "/" + repoName + ".git",
214
                "/" + repoName + "/.git",
215
        }
216
        for _, dir := range tryDirs </span><span class="cov8" title="31">{
217
                if fileInfo, err := os.Stat(theConfig.Root + dir); err != nil </span><span class="cov7" title="18">{
218
                        if !os.IsNotExist(err) </span><span class="cov0" title="0">{
219
                                statusCode, statusText = http.StatusInternalServerError, err.Error()
220
                                return
221
                        }</span>
222
                }<span class="cov6" title="13"> else if fileInfo.IsDir() </span><span class="cov6" title="13">{
223
                        rewrittenPath = dir + "/" + pathParts[1]
224
                        break</span>
225
                }
226
        }
227
        <span class="cov6" title="15">if rewrittenPath == "" </span><span class="cov2" title="2">{
228
                log.Println("WARNING:", repoUUID,
229
                        "git directory not found in", theConfig.Root, tryDirs)
230
                // We say "content not found" to disambiguate from the
231
                // earlier "API says that repo does not exist" error.
232
                statusCode, statusText = http.StatusNotFound, "content not found"
233
                return
234
        }</span>
235
        <span class="cov6" title="13">r.URL.Path = rewrittenPath
236

    
237
        h.handler.ServeHTTP(&amp;w, r)</span>
238
}
239
</pre>
240
		
241
		<pre class="file" id="file1" style="display: none">package main
242

    
243
import (
244
        "log"
245
        "net"
246
        "net/http"
247
        "net/http/cgi"
248
)
249

    
250
// gitHandler is an http.Handler that invokes git-http-backend (or
251
// whatever backend is configured) via CGI, with appropriate
252
// environment variables in place for git-http-backend or
253
// gitolite-shell.
254
type gitHandler struct {
255
        cgi.Handler
256
}
257

    
258
func newGitHandler() http.Handler <span class="cov9" title="12">{
259
        return &amp;gitHandler{
260
                Handler: cgi.Handler{
261
                        Path: theConfig.GitCommand,
262
                        Dir:  theConfig.Root,
263
                        Env: []string{
264
                                "GIT_PROJECT_ROOT=" + theConfig.Root,
265
                                "GIT_HTTP_EXPORT_ALL=",
266
                                "SERVER_ADDR=" + theConfig.Addr,
267
                        },
268
                        InheritEnv: []string{
269
                                "PATH",
270
                                // Needed if GitCommand is gitolite-shell:
271
                                "GITOLITE_HTTP_HOME",
272
                                "GL_BYPASS_ACCESS_CHECKS",
273
                        },
274
                        Args: []string{"http-backend"},
275
                },
276
        }
277
}</span>
278

    
279
func (h *gitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) <span class="cov10" title="15">{
280
        remoteHost, remotePort, err := net.SplitHostPort(r.RemoteAddr)
281
        if err != nil </span><span class="cov1" title="1">{
282
                log.Printf("Internal error: SplitHostPort(r.RemoteAddr==%q): %s", r.RemoteAddr, err)
283
                w.WriteHeader(http.StatusInternalServerError)
284
                return
285
        }</span>
286

    
287
        // Copy the wrapped cgi.Handler, so these request-specific
288
        // variables don't leak into the next request.
289
        <span class="cov9" title="14">handlerCopy := h.Handler
290
        handlerCopy.Env = append(handlerCopy.Env,
291
                // In Go1.5 we can skip this, net/http/cgi will do it for us:
292
                "REMOTE_HOST="+remoteHost,
293
                "REMOTE_ADDR="+remoteHost,
294
                "REMOTE_PORT="+remotePort,
295
                // Ideally this would be a real username:
296
                "REMOTE_USER="+r.RemoteAddr,
297
        )
298
        handlerCopy.ServeHTTP(w, r)</span>
299
}
300
</pre>
301
		
302
		<pre class="file" id="file2" style="display: none">package main
303

    
304
import (
305
        "flag"
306
        "log"
307
        "os"
308
)
309

    
310
type config struct {
311
        Addr       string
312
        GitCommand string
313
        Root       string
314
}
315

    
316
var theConfig *config
317

    
318
func init() <span class="cov8" title="1">{
319
        theConfig = &amp;config{}
320
        flag.StringVar(&amp;theConfig.Addr, "address", "0.0.0.0:80",
321
                "Address to listen on, \"host:port\".")
322
        flag.StringVar(&amp;theConfig.GitCommand, "git-command", "/usr/bin/git",
323
                "Path to git or gitolite-shell executable. Each authenticated request will execute this program with a single argument, \"http-backend\".")
324
        cwd, err := os.Getwd()
325
        if err != nil </span><span class="cov0" title="0">{
326
                log.Fatalln("Getwd():", err)
327
        }</span>
328
        <span class="cov8" title="1">flag.StringVar(&amp;theConfig.Root, "repo-root", cwd,
329
                "Path to git repositories.")
330

    
331
        // MakeArvadosClient returns an error if token is unset (even
332
        // though we don't need to do anything requiring
333
        // authentication yet). We can't do this in newArvadosClient()
334
        // just before calling MakeArvadosClient(), though, because
335
        // that interferes with the env var needed by "run test
336
        // servers".
337
        os.Setenv("ARVADOS_API_TOKEN", "xxx")</span>
338
}
339

    
340
func main() <span class="cov0" title="0">{
341
        flag.Parse()
342
        srv := &amp;server{}
343
        if err := srv.Start(); err != nil </span><span class="cov0" title="0">{
344
                log.Fatal(err)
345
        }</span>
346
        <span class="cov0" title="0">log.Println("Listening at", srv.Addr)
347
        log.Println("Repository root", theConfig.Root)
348
        if err := srv.Wait(); err != nil </span><span class="cov0" title="0">{
349
                log.Fatal(err)
350
        }</span>
351
}
352
</pre>
353
		
354
		<pre class="file" id="file3" style="display: none">package main
355

    
356
import (
357
        "net/http"
358

    
359
        "git.curoverse.com/arvados.git/sdk/go/httpserver"
360
)
361

    
362
type server struct {
363
        httpserver.Server
364
}
365

    
366
func (srv *server) Start() error <span class="cov10" title="10">{
367
        mux := http.NewServeMux()
368
        mux.Handle("/", &amp;authHandler{newGitHandler()})
369
        srv.Handler = mux
370
        srv.Addr = theConfig.Addr
371
        return srv.Server.Start()
372
}</span>
373
</pre>
374
		
375
		</div>
376
	</body>
377
	<script>
378
	(function() {
379
		var files = document.getElementById('files');
380
		var visible = document.getElementById('file0');
381
		files.addEventListener('change', onChange, false);
382
		function onChange() {
383
			visible.style.display = 'none';
384
			visible = document.getElementById(files.value);
385
			visible.style.display = 'block';
386
			window.scrollTo(0, 0);
387
		}
388
	})();
389
	</script>
390
</html>
    (1-1/1)