3220: fix tests
[arvados.git] / services / keep / src / keep / keep.go
1 package main
2
3 import (
4         "bufio"
5         "bytes"
6         "crypto/md5"
7         "encoding/json"
8         "flag"
9         "fmt"
10         "github.com/gorilla/mux"
11         "io"
12         "io/ioutil"
13         "log"
14         "net"
15         "net/http"
16         "os"
17         "os/signal"
18         "regexp"
19         "runtime"
20         "strconv"
21         "strings"
22         "syscall"
23         "time"
24 )
25
26 // ======================
27 // Configuration settings
28 //
29 // TODO(twp): make all of these configurable via command line flags
30 // and/or configuration file settings.
31
32 // Default TCP address on which to listen for requests.
33 // Initialized by the --listen flag.
34 const DEFAULT_ADDR = ":25107"
35
36 // A Keep "block" is 64MB.
37 const BLOCKSIZE = 64 * 1024 * 1024
38
39 // A Keep volume must have at least MIN_FREE_KILOBYTES available
40 // in order to permit writes.
41 const MIN_FREE_KILOBYTES = BLOCKSIZE / 1024
42
43 var PROC_MOUNTS = "/proc/mounts"
44
45 // The Keep VolumeManager maintains a list of available volumes.
46 // Initialized by the --volumes flag (or by FindKeepVolumes).
47 var KeepVM VolumeManager
48
49 // enforce_permissions controls whether permission signatures
50 // should be enforced (affecting GET and DELETE requests).
51 // Initialized by the --enforce-permissions flag.
52 var enforce_permissions bool
53
54 // permission_ttl is the time duration for which new permission
55 // signatures (returned by PUT requests) will be valid.
56 // Initialized by the --permission-ttl flag.
57 var permission_ttl time.Duration
58
59 // data_manager_token represents the API token used by the
60 // Data Manager, and is required on certain privileged operations.
61 // Initialized by the --data-manager-token-file flag.
62 var data_manager_token string
63
64 // ==========
65 // Error types.
66 //
67 type KeepError struct {
68         HTTPCode int
69         ErrMsg   string
70 }
71
72 var (
73         BadRequestError = &KeepError{400, "Bad Request"}
74         CollisionError  = &KeepError{500, "Collision"}
75         RequestHashError= &KeepError{422, "Hash mismatch in request"}
76         PermissionError = &KeepError{403, "Forbidden"}
77         DiskHashError   = &KeepError{500, "Hash mismatch in stored data"}
78         ExpiredError    = &KeepError{401, "Expired permission signature"}
79         NotFoundError   = &KeepError{404, "Not Found"}
80         GenericError    = &KeepError{500, "Fail"}
81         FullError       = &KeepError{503, "Full"}
82         TooLongError    = &KeepError{504, "Timeout"}
83 )
84
85 func (e *KeepError) Error() string {
86         return e.ErrMsg
87 }
88
89 // TODO(twp): continue moving as much code as possible out of main
90 // so it can be effectively tested. Esp. handling and postprocessing
91 // of command line flags (identifying Keep volumes and initializing
92 // permission arguments).
93
94 func main() {
95         log.Println("Keep started: pid", os.Getpid())
96
97         // Parse command-line flags:
98         //
99         // -listen=ipaddr:port
100         //    Interface on which to listen for requests. Use :port without
101         //    an ipaddr to listen on all network interfaces.
102         //    Examples:
103         //      -listen=127.0.0.1:4949
104         //      -listen=10.0.1.24:8000
105         //      -listen=:25107 (to listen to port 25107 on all interfaces)
106         //
107         // -volumes
108         //    A comma-separated list of directories to use as Keep volumes.
109         //    Example:
110         //      -volumes=/var/keep01,/var/keep02,/var/keep03/subdir
111         //
112         //    If -volumes is empty or is not present, Keep will select volumes
113         //    by looking at currently mounted filesystems for /keep top-level
114         //    directories.
115
116         var (
117                 data_manager_token_file string
118                 listen                  string
119                 permission_key_file     string
120                 permission_ttl_sec      int
121                 serialize_io            bool
122                 volumearg               string
123                 pidfile                 string
124         )
125         flag.StringVar(
126                 &data_manager_token_file,
127                 "data-manager-token-file",
128                 "",
129                 "File with the API token used by the Data Manager. All DELETE "+
130                         "requests or GET /index requests must carry this token.")
131         flag.BoolVar(
132                 &enforce_permissions,
133                 "enforce-permissions",
134                 false,
135                 "Enforce permission signatures on requests.")
136         flag.StringVar(
137                 &listen,
138                 "listen",
139                 DEFAULT_ADDR,
140                 "Interface on which to listen for requests, in the format "+
141                         "ipaddr:port. e.g. -listen=10.0.1.24:8000. Use -listen=:port "+
142                         "to listen on all network interfaces.")
143         flag.StringVar(
144                 &permission_key_file,
145                 "permission-key-file",
146                 "",
147                 "File containing the secret key for generating and verifying "+
148                         "permission signatures.")
149         flag.IntVar(
150                 &permission_ttl_sec,
151                 "permission-ttl",
152                 1209600,
153                 "Expiration time (in seconds) for newly generated permission "+
154                         "signatures.")
155         flag.BoolVar(
156                 &serialize_io,
157                 "serialize",
158                 false,
159                 "If set, all read and write operations on local Keep volumes will "+
160                         "be serialized.")
161         flag.StringVar(
162                 &volumearg,
163                 "volumes",
164                 "",
165                 "Comma-separated list of directories to use for Keep volumes, "+
166                         "e.g. -volumes=/var/keep1,/var/keep2. If empty or not "+
167                         "supplied, Keep will scan mounted filesystems for volumes "+
168                         "with a /keep top-level directory.")
169
170         flag.StringVar(
171                 &pidfile,
172                 "pid",
173                 "",
174                 "Path to write pid file")
175
176         flag.Parse()
177
178         // Look for local keep volumes.
179         var keepvols []string
180         if volumearg == "" {
181                 // TODO(twp): decide whether this is desirable default behavior.
182                 // In production we may want to require the admin to specify
183                 // Keep volumes explicitly.
184                 keepvols = FindKeepVolumes()
185         } else {
186                 keepvols = strings.Split(volumearg, ",")
187         }
188
189         // Check that the specified volumes actually exist.
190         var goodvols []Volume = nil
191         for _, v := range keepvols {
192                 if _, err := os.Stat(v); err == nil {
193                         log.Println("adding Keep volume:", v)
194                         newvol := MakeUnixVolume(v, serialize_io)
195                         goodvols = append(goodvols, &newvol)
196                 } else {
197                         log.Printf("bad Keep volume: %s\n", err)
198                 }
199         }
200
201         if len(goodvols) == 0 {
202                 log.Fatal("could not find any keep volumes")
203         }
204
205         // Initialize data manager token and permission key.
206         // If these tokens are specified but cannot be read,
207         // raise a fatal error.
208         if data_manager_token_file != "" {
209                 if buf, err := ioutil.ReadFile(data_manager_token_file); err == nil {
210                         data_manager_token = strings.TrimSpace(string(buf))
211                 } else {
212                         log.Fatalf("reading data manager token: %s\n", err)
213                 }
214         }
215         if permission_key_file != "" {
216                 if buf, err := ioutil.ReadFile(permission_key_file); err == nil {
217                         PermissionSecret = bytes.TrimSpace(buf)
218                 } else {
219                         log.Fatalf("reading permission key: %s\n", err)
220                 }
221         }
222
223         // Initialize permission TTL
224         permission_ttl = time.Duration(permission_ttl_sec) * time.Second
225
226         // If --enforce-permissions is true, we must have a permission key
227         // to continue.
228         if PermissionSecret == nil {
229                 if enforce_permissions {
230                         log.Fatal("--enforce-permissions requires a permission key")
231                 } else {
232                         log.Println("Running without a PermissionSecret. Block locators " +
233                                 "returned by this server will not be signed, and will be rejected " +
234                                 "by a server that enforces permissions.")
235                         log.Println("To fix this, run Keep with --permission-key-file=<path> " +
236                                 "to define the location of a file containing the permission key.")
237                 }
238         }
239
240         // Start a round-robin VolumeManager with the volumes we have found.
241         KeepVM = MakeRRVolumeManager(goodvols)
242
243         // Tell the built-in HTTP server to direct all requests to the REST
244         // router.
245         http.Handle("/", MakeRESTRouter())
246
247         // Set up a TCP listener.
248         listener, err := net.Listen("tcp", listen)
249         if err != nil {
250                 log.Fatal(err)
251         }
252
253         // Shut down the server gracefully (by closing the listener)
254         // if SIGTERM is received.
255         term := make(chan os.Signal, 1)
256         go func(sig <-chan os.Signal) {
257                 s := <-sig
258                 log.Println("caught signal:", s)
259                 listener.Close()
260         }(term)
261         signal.Notify(term, syscall.SIGTERM)
262
263         if pidfile != "" {
264                 f, err := os.Create(pidfile)
265                 if err == nil {
266                         fmt.Fprint(f, os.Getpid())
267                         f.Close()
268                 } else {
269                         log.Printf("Error writing pid file (%s): %s", pidfile, err.Error())
270                 }
271         }
272
273         // Start listening for requests.
274         srv := &http.Server{Addr: listen}
275         srv.Serve(listener)
276
277         log.Println("shutting down")
278
279         if pidfile != "" {
280                 os.Remove(pidfile)
281         }
282 }
283
284 // MakeRESTRouter
285 //     Returns a mux.Router that passes GET and PUT requests to the
286 //     appropriate handlers.
287 //
288 func MakeRESTRouter() *mux.Router {
289         rest := mux.NewRouter()
290
291         rest.HandleFunc(
292                 `/{hash:[0-9a-f]{32}}`, GetBlockHandler).Methods("GET", "HEAD")
293         rest.HandleFunc(
294                 `/{hash:[0-9a-f]{32}}+{hints}`,
295                 GetBlockHandler).Methods("GET", "HEAD")
296
297         rest.HandleFunc(`/{hash:[0-9a-f]{32}}`, PutBlockHandler).Methods("PUT")
298
299         // For IndexHandler we support:
300         //   /index           - returns all locators
301         //   /index/{prefix}  - returns all locators that begin with {prefix}
302         //      {prefix} is a string of hexadecimal digits between 0 and 32 digits.
303         //      If {prefix} is the empty string, return an index of all locators
304         //      (so /index and /index/ behave identically)
305         //      A client may supply a full 32-digit locator string, in which
306         //      case the server will return an index with either zero or one
307         //      entries. This usage allows a client to check whether a block is
308         //      present, and its size and upload time, without retrieving the
309         //      entire block.
310         //
311         rest.HandleFunc(`/index`, IndexHandler).Methods("GET", "HEAD")
312         rest.HandleFunc(
313                 `/index/{prefix:[0-9a-f]{0,32}}`, IndexHandler).Methods("GET", "HEAD")
314         rest.HandleFunc(`/status.json`, StatusHandler).Methods("GET", "HEAD")
315
316         // Any request which does not match any of these routes gets
317         // 400 Bad Request.
318         rest.NotFoundHandler = http.HandlerFunc(BadRequestHandler)
319
320         return rest
321 }
322
323 func BadRequestHandler(w http.ResponseWriter, r *http.Request) {
324         http.Error(w, BadRequestError.Error(), BadRequestError.HTTPCode)
325 }
326
327 // FindKeepVolumes
328 //     Returns a list of Keep volumes mounted on this system.
329 //
330 //     A Keep volume is a normal or tmpfs volume with a /keep
331 //     directory at the top level of the mount point.
332 //
333 func FindKeepVolumes() []string {
334         vols := make([]string, 0)
335
336         if f, err := os.Open(PROC_MOUNTS); err != nil {
337                 log.Fatalf("opening %s: %s\n", PROC_MOUNTS, err)
338         } else {
339                 scanner := bufio.NewScanner(f)
340                 for scanner.Scan() {
341                         args := strings.Fields(scanner.Text())
342                         dev, mount := args[0], args[1]
343                         if mount != "/" &&
344                                 (dev == "tmpfs" || strings.HasPrefix(dev, "/dev/")) {
345                                 keep := mount + "/keep"
346                                 if st, err := os.Stat(keep); err == nil && st.IsDir() {
347                                         vols = append(vols, keep)
348                                 }
349                         }
350                 }
351                 if err := scanner.Err(); err != nil {
352                         log.Fatal(err)
353                 }
354         }
355         return vols
356 }
357
358 func GetBlockHandler(resp http.ResponseWriter, req *http.Request) {
359         hash := mux.Vars(req)["hash"]
360
361         log.Printf("%s %s", req.Method, hash)
362
363         hints := mux.Vars(req)["hints"]
364
365         // Parse the locator string and hints from the request.
366         // TODO(twp): implement a Locator type.
367         var signature, timestamp string
368         if hints != "" {
369                 signature_pat, _ := regexp.Compile("^A([[:xdigit:]]+)@([[:xdigit:]]{8})$")
370                 for _, hint := range strings.Split(hints, "+") {
371                         if match, _ := regexp.MatchString("^[[:digit:]]+$", hint); match {
372                                 // Server ignores size hints
373                         } else if m := signature_pat.FindStringSubmatch(hint); m != nil {
374                                 signature = m[1]
375                                 timestamp = m[2]
376                         } else if match, _ := regexp.MatchString("^[[:upper:]]", hint); match {
377                                 // Any unknown hint that starts with an uppercase letter is
378                                 // presumed to be valid and ignored, to permit forward compatibility.
379                         } else {
380                                 // Unknown format; not a valid locator.
381                                 http.Error(resp, BadRequestError.Error(), BadRequestError.HTTPCode)
382                                 return
383                         }
384                 }
385         }
386
387         // If permission checking is in effect, verify this
388         // request's permission signature.
389         if enforce_permissions {
390                 if signature == "" || timestamp == "" {
391                         http.Error(resp, PermissionError.Error(), PermissionError.HTTPCode)
392                         return
393                 } else if IsExpired(timestamp) {
394                         http.Error(resp, ExpiredError.Error(), ExpiredError.HTTPCode)
395                         return
396                 } else {
397                         req_locator := req.URL.Path[1:] // strip leading slash
398                         if !VerifySignature(req_locator, GetApiToken(req)) {
399                                 http.Error(resp, PermissionError.Error(), PermissionError.HTTPCode)
400                                 return
401                         }
402                 }
403         }
404
405         block, err := GetBlock(hash)
406
407         // Garbage collect after each GET. Fixes #2865.
408         // TODO(twp): review Keep memory usage and see if there's
409         // a better way to do this than blindly garbage collecting
410         // after every block.
411         defer runtime.GC()
412
413         if err != nil {
414                 // This type assertion is safe because the only errors
415                 // GetBlock can return are DiskHashError or NotFoundError.
416                 if err == NotFoundError {
417                         log.Printf("%s: not found, giving up\n", hash)
418                 }
419                 http.Error(resp, err.Error(), err.(*KeepError).HTTPCode)
420                 return
421         }
422
423         resp.Header().Set("X-Block-Size", fmt.Sprintf("%d", len(block)))
424
425         _, err = resp.Write(block)
426         if err != nil {
427                 log.Printf("GetBlockHandler: writing response: %s", err)
428         }
429
430         return
431 }
432
433 func PutBlockHandler(resp http.ResponseWriter, req *http.Request) {
434         // Garbage collect after each PUT. Fixes #2865.
435         // See also GetBlockHandler.
436         defer runtime.GC()
437
438         hash := mux.Vars(req)["hash"]
439
440         log.Printf("%s %s", req.Method, hash)
441
442         // Read the block data to be stored.
443         // If the request exceeds BLOCKSIZE bytes, issue a HTTP 500 error.
444         //
445         if req.ContentLength > BLOCKSIZE {
446                 http.Error(resp, TooLongError.Error(), TooLongError.HTTPCode)
447                 return
448         }
449
450         buf := make([]byte, req.ContentLength)
451         nread, err := io.ReadFull(req.Body, buf)
452         if err != nil {
453                 http.Error(resp, err.Error(), 500)
454         } else if int64(nread) < req.ContentLength {
455                 http.Error(resp, "request truncated", 500)
456         } else {
457                 if err := PutBlock(buf, hash); err == nil {
458                         // Success; add a size hint, sign the locator if
459                         // possible, and return it to the client.
460                         return_hash := fmt.Sprintf("%s+%d", hash, len(buf))
461                         api_token := GetApiToken(req)
462                         if PermissionSecret != nil && api_token != "" {
463                                 expiry := time.Now().Add(permission_ttl)
464                                 return_hash = SignLocator(return_hash, api_token, expiry)
465                         }
466                         resp.Write([]byte(return_hash + "\n"))
467                 } else {
468                         ke := err.(*KeepError)
469                         http.Error(resp, ke.Error(), ke.HTTPCode)
470                 }
471         }
472         return
473 }
474
475 // IndexHandler
476 //     A HandleFunc to address /index and /index/{prefix} requests.
477 //
478 func IndexHandler(resp http.ResponseWriter, req *http.Request) {
479         prefix := mux.Vars(req)["prefix"]
480
481         // Only the data manager may issue /index requests,
482         // and only if enforce_permissions is enabled.
483         // All other requests return 403 Forbidden.
484         api_token := GetApiToken(req)
485         if !enforce_permissions ||
486                 api_token == "" ||
487                 data_manager_token != api_token {
488                 http.Error(resp, PermissionError.Error(), PermissionError.HTTPCode)
489                 return
490         }
491         var index string
492         for _, vol := range KeepVM.Volumes() {
493                 index = index + vol.Index(prefix)
494         }
495         resp.Write([]byte(index))
496 }
497
498 // StatusHandler
499 //     Responds to /status.json requests with the current node status,
500 //     described in a JSON structure.
501 //
502 //     The data given in a status.json response includes:
503 //        volumes - a list of Keep volumes currently in use by this server
504 //          each volume is an object with the following fields:
505 //            * mount_point
506 //            * device_num (an integer identifying the underlying filesystem)
507 //            * bytes_free
508 //            * bytes_used
509 //
510 type VolumeStatus struct {
511         MountPoint string `json:"mount_point"`
512         DeviceNum  uint64 `json:"device_num"`
513         BytesFree  uint64 `json:"bytes_free"`
514         BytesUsed  uint64 `json:"bytes_used"`
515 }
516
517 type NodeStatus struct {
518         Volumes []*VolumeStatus `json:"volumes"`
519 }
520
521 func StatusHandler(resp http.ResponseWriter, req *http.Request) {
522         st := GetNodeStatus()
523         if jstat, err := json.Marshal(st); err == nil {
524                 resp.Write(jstat)
525         } else {
526                 log.Printf("json.Marshal: %s\n", err)
527                 log.Printf("NodeStatus = %v\n", st)
528                 http.Error(resp, err.Error(), 500)
529         }
530 }
531
532 // GetNodeStatus
533 //     Returns a NodeStatus struct describing this Keep
534 //     node's current status.
535 //
536 func GetNodeStatus() *NodeStatus {
537         st := new(NodeStatus)
538
539         st.Volumes = make([]*VolumeStatus, len(KeepVM.Volumes()))
540         for i, vol := range KeepVM.Volumes() {
541                 st.Volumes[i] = vol.Status()
542         }
543         return st
544 }
545
546 // GetVolumeStatus
547 //     Returns a VolumeStatus describing the requested volume.
548 //
549 func GetVolumeStatus(volume string) *VolumeStatus {
550         var fs syscall.Statfs_t
551         var devnum uint64
552
553         if fi, err := os.Stat(volume); err == nil {
554                 devnum = fi.Sys().(*syscall.Stat_t).Dev
555         } else {
556                 log.Printf("GetVolumeStatus: os.Stat: %s\n", err)
557                 return nil
558         }
559
560         err := syscall.Statfs(volume, &fs)
561         if err != nil {
562                 log.Printf("GetVolumeStatus: statfs: %s\n", err)
563                 return nil
564         }
565         // These calculations match the way df calculates disk usage:
566         // "free" space is measured by fs.Bavail, but "used" space
567         // uses fs.Blocks - fs.Bfree.
568         free := fs.Bavail * uint64(fs.Bsize)
569         used := (fs.Blocks - fs.Bfree) * uint64(fs.Bsize)
570         return &VolumeStatus{volume, devnum, free, used}
571 }
572
573 func GetBlock(hash string) ([]byte, error) {
574         // Attempt to read the requested hash from a keep volume.
575         error_to_caller := NotFoundError
576
577         for _, vol := range KeepVM.Volumes() {
578                 if buf, err := vol.Get(hash); err != nil {
579                         // IsNotExist is an expected error and may be ignored.
580                         // (If all volumes report IsNotExist, we return a NotFoundError)
581                         // All other errors should be logged but we continue trying to
582                         // read.
583                         switch {
584                         case os.IsNotExist(err):
585                                 continue
586                         default:
587                                 log.Printf("GetBlock: reading %s: %s\n", hash, err)
588                         }
589                 } else {
590                         // Double check the file checksum.
591                         //
592                         filehash := fmt.Sprintf("%x", md5.Sum(buf))
593                         if filehash != hash {
594                                 // TODO(twp): this condition probably represents a bad disk and
595                                 // should raise major alarm bells for an administrator: e.g.
596                                 // they should be sent directly to an event manager at high
597                                 // priority or logged as urgent problems.
598                                 //
599                                 log.Printf("%s: checksum mismatch for request %s (actual %s)\n",
600                                         vol, hash, filehash)
601                                 error_to_caller = DiskHashError
602                         } else {
603                                 // Success!
604                                 if error_to_caller != NotFoundError {
605                                                 log.Printf("%s: checksum mismatch for request %s but a good copy was found on another volume and returned\n",
606                                                         vol, hash)
607                                 }
608                                 return buf, nil
609                         }
610                 }
611         }
612
613   if error_to_caller != NotFoundError {
614     log.Printf("%s: checksum mismatch, no good copy found\n", hash)
615   }
616         return nil, error_to_caller
617 }
618
619 /* PutBlock(block, hash)
620    Stores the BLOCK (identified by the content id HASH) in Keep.
621
622    The MD5 checksum of the block must be identical to the content id HASH.
623    If not, an error is returned.
624
625    PutBlock stores the BLOCK on the first Keep volume with free space.
626    A failure code is returned to the user only if all volumes fail.
627
628    On success, PutBlock returns nil.
629    On failure, it returns a KeepError with one of the following codes:
630
631    500 Collision
632           A different block with the same hash already exists on this
633           Keep server.
634    422 MD5Fail
635           The MD5 hash of the BLOCK does not match the argument HASH.
636    503 Full
637           There was not enough space left in any Keep volume to store
638           the object.
639    500 Fail
640           The object could not be stored for some other reason (e.g.
641           all writes failed). The text of the error message should
642           provide as much detail as possible.
643 */
644
645 func PutBlock(block []byte, hash string) error {
646         // Check that BLOCK's checksum matches HASH.
647         blockhash := fmt.Sprintf("%x", md5.Sum(block))
648         if blockhash != hash {
649                 log.Printf("%s: MD5 checksum %s did not match request", hash, blockhash)
650                 return RequestHashError
651         }
652
653         // If we already have a block on disk under this identifier, return
654         // success (but check for MD5 collisions).
655         // The only errors that GetBlock can return are DiskHashError and NotFoundError.
656         // In either case, we want to write our new (good) block to disk,
657         // so there is nothing special to do if err != nil.
658         if oldblock, err := GetBlock(hash); err == nil {
659                 if bytes.Compare(block, oldblock) == 0 {
660                         return nil
661                 } else {
662                         return CollisionError
663                 }
664         }
665
666         // Choose a Keep volume to write to.
667         // If this volume fails, try all of the volumes in order.
668         vol := KeepVM.Choose()
669         if err := vol.Put(hash, block); err == nil {
670                 return nil // success!
671         } else {
672                 allFull := true
673                 for _, vol := range KeepVM.Volumes() {
674                         err := vol.Put(hash, block)
675                         if err == nil {
676                                 return nil // success!
677                         }
678                         if err != FullError {
679                                 // The volume is not full but the write did not succeed.
680                                 // Report the error and continue trying.
681                                 allFull = false
682                                 log.Printf("%s: Write(%s): %s\n", vol, hash, err)
683                         }
684                 }
685
686                 if allFull {
687                         log.Printf("all Keep volumes full")
688                         return FullError
689                 } else {
690                         log.Printf("all Keep volumes failed")
691                         return GenericError
692                 }
693         }
694 }
695
696 // IsValidLocator
697 //     Return true if the specified string is a valid Keep locator.
698 //     When Keep is extended to support hash types other than MD5,
699 //     this should be updated to cover those as well.
700 //
701 func IsValidLocator(loc string) bool {
702         match, err := regexp.MatchString(`^[0-9a-f]{32}$`, loc)
703         if err == nil {
704                 return match
705         }
706         log.Printf("IsValidLocator: %s\n", err)
707         return false
708 }
709
710 // GetApiToken returns the OAuth2 token from the Authorization
711 // header of a HTTP request, or an empty string if no matching
712 // token is found.
713 func GetApiToken(req *http.Request) string {
714         if auth, ok := req.Header["Authorization"]; ok {
715                 if pat, err := regexp.Compile(`^OAuth2\s+(.*)`); err != nil {
716                         log.Println(err)
717                 } else if match := pat.FindStringSubmatch(auth[0]); match != nil {
718                         return match[1]
719                 }
720         }
721         return ""
722 }
723
724 // IsExpired returns true if the given Unix timestamp (expressed as a
725 // hexadecimal string) is in the past, or if timestamp_hex cannot be
726 // parsed as a hexadecimal string.
727 func IsExpired(timestamp_hex string) bool {
728         ts, err := strconv.ParseInt(timestamp_hex, 16, 0)
729         if err != nil {
730                 log.Printf("IsExpired: %s\n", err)
731                 return true
732         }
733         return time.Unix(ts, 0).Before(time.Now())
734 }