github.com/hoffie/larasync@v0.0.0-20151025221940-0384d2bddcef/api/server/server.go (about) 1 package server 2 3 import ( 4 "crypto/tls" 5 "fmt" 6 "net" 7 "net/http" 8 "os" 9 "time" 10 11 "github.com/gorilla/mux" 12 "github.com/inconshreveable/log15" 13 14 "github.com/hoffie/larasync/api/common" 15 "github.com/hoffie/larasync/helpers/lock" 16 "github.com/hoffie/larasync/helpers/x509" 17 "github.com/hoffie/larasync/repository" 18 ) 19 20 const ( 21 // DefaultPort specifies the server's default TCP port 22 DefaultPort = 14124 23 ) 24 25 // Server represents our http environment. 26 type Server struct { 27 adminPubkey [PublicKeySize]byte 28 router *mux.Router 29 maxRequestAge time.Duration 30 http *http.Server 31 certFile string 32 keyFile string 33 certificate tls.Certificate 34 rm *repository.Manager 35 } 36 37 // New returns a new Server. 38 func New(adminPubkey [PublicKeySize]byte, maxRequestAge time.Duration, rm *repository.Manager, certFile, keyFile string) (*Server, error) { 39 serveMux := http.NewServeMux() 40 s := Server{ 41 adminPubkey: adminPubkey, 42 maxRequestAge: maxRequestAge, 43 rm: rm, 44 router: mux.NewRouter(), 45 http: &http.Server{ 46 Addr: fmt.Sprintf(":%d", DefaultPort), 47 Handler: serveMux, 48 }, 49 certFile: certFile, 50 keyFile: keyFile, 51 } 52 s.setupRoutes() 53 err := s.loadCertificate() 54 if err != nil { 55 return nil, err 56 } 57 serveMux.Handle("/", s.router) 58 return &s, nil 59 } 60 61 func jsonHeader(rw http.ResponseWriter) { 62 rw.Header().Set("Content-Type", "application/json") 63 } 64 65 // setupRoutes is responsible for registering API endpoints. 66 func (s *Server) setupRoutes() { 67 s.router.HandleFunc("/repositories", 68 s.requireAdminAuth(s.repositoryList)).Methods("GET") 69 s.router.HandleFunc("/repositories/{repository}", 70 s.requireAdminAuth(s.repositoryCreate)).Methods("PUT") 71 72 s.router.HandleFunc("/repositories/{repository}/blobs/{blobID}", 73 s.requireRepositoryAuth(s.blobGet)).Methods("GET") 74 s.router.HandleFunc("/repositories/{repository}/blobs/{blobID}", 75 s.requireRepositoryAuth(s.blobPut)).Methods("PUT") 76 77 s.router.HandleFunc("/repositories/{repository}/nibs", 78 s.requireRepositoryAuth(s.nibList)).Methods("GET") 79 s.router.HandleFunc("/repositories/{repository}/nibs/{nibID}", 80 s.requireRepositoryAuth(s.nibGet)).Methods("GET") 81 s.router.HandleFunc("/repositories/{repository}/nibs/{nibID}", 82 s.requireRepositoryAuth( 83 s.synchronizeWith( 84 "nibPUT", 85 s.checkTransactionPrecondition(s.nibPut), 86 ), 87 ), 88 ).Methods("PUT") 89 90 s.router.HandleFunc("/repositories/{repository}/authorizations/{authPublicKey}", 91 s.authorizationGet).Methods("GET") 92 s.router.HandleFunc("/repositories/{repository}/authorizations/{authPublicKey}", 93 s.requireRepositoryAuth(s.authorizationPut)).Methods("PUT") 94 95 s.router.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { 96 rw.Write([]byte("larasync\n")) 97 }) 98 } 99 100 // requireAdminAuth wraps a HandlerFunc and only calls it if the request has a 101 // valid admin auth header 102 func (s *Server) requireAdminAuth(f http.HandlerFunc) http.HandlerFunc { 103 return func(rw http.ResponseWriter, req *http.Request) { 104 if !common.ValidateRequest(req, s.adminPubkey, s.maxRequestAge) { 105 http.Error(rw, "Unauthorized", http.StatusUnauthorized) 106 return 107 } 108 f(rw, req) 109 } 110 } 111 112 // requireAuth wraps a handlerFunc and only calls it if the request is 113 // authenticated 114 func (s *Server) requireRepositoryAuth(f http.HandlerFunc) http.HandlerFunc { 115 return func(rw http.ResponseWriter, req *http.Request) { 116 vars := mux.Vars(req) 117 repositoryName := vars["repository"] 118 repository, err := s.rm.Open(repositoryName) 119 if err != nil { 120 if os.IsNotExist(err) { 121 // Repository is not found. However, due to security reasons 122 // we are returning Unauthorized here so that an unauthenticated user 123 // cannot check wether a repository does exist or not. 124 // FIXME: timing side channel 125 http.Error(rw, "Unauthorized", http.StatusUnauthorized) 126 } else { 127 http.Error(rw, "Internal Error", http.StatusInternalServerError) 128 } 129 return 130 } 131 132 var pubKeyArray [PublicKeySize]byte 133 pubKey, err := repository.GetSigningPublicKey() 134 if err != nil { 135 http.Error(rw, "Internal Error", http.StatusInternalServerError) 136 return 137 } 138 139 copy(pubKeyArray[0:PublicKeySize], pubKey[:PublicKeySize]) 140 // TODO: Find if there is a better way for this. 141 142 if !common.ValidateRequest(req, pubKeyArray, s.maxRequestAge) { 143 http.Error(rw, "Unauthorized", http.StatusUnauthorized) 144 return 145 } 146 147 f(rw, req) 148 } 149 } 150 151 // synchronizeWith can be used as a wrapper. The endpoint will then be synchronized 152 // via the lock manager and the given role and in the given repository. 153 func (s *Server) synchronizeWith(roleName string, f http.HandlerFunc) http.HandlerFunc { 154 return func(rw http.ResponseWriter, req *http.Request) { 155 vars := mux.Vars(req) 156 repositoryName := vars["repository"] 157 repository, err := s.rm.Open(repositoryName) 158 if err != nil { 159 http.Error(rw, "Internal Error", http.StatusInternalServerError) 160 return 161 } 162 163 manager := lock.CurrentManager() 164 lock := manager.Get( 165 repository.GetManagementDir(), 166 fmt.Sprintf("api:%s", roleName), 167 ) 168 169 lock.Lock() 170 f(rw, req) 171 lock.Unlock() 172 } 173 } 174 175 // checkTransactionPrecondition checks if there is a if-match header set that this 176 // entry is the current transaction id in the system. 177 func (s *Server) checkTransactionPrecondition(f http.HandlerFunc) http.HandlerFunc { 178 return func(rw http.ResponseWriter, req *http.Request) { 179 vars := mux.Vars(req) 180 repositoryName := vars["repository"] 181 repository, err := s.rm.Open(repositoryName) 182 if err != nil { 183 http.Error(rw, "Internal Error", http.StatusInternalServerError) 184 return 185 } 186 187 header := req.Header 188 ifMatch := header.Get("If-Match") 189 if ifMatch != "" { 190 currentTransaction, err := repository.CurrentTransaction() 191 if err != nil { 192 http.Error(rw, "Internal Error", http.StatusInternalServerError) 193 return 194 } 195 196 if ifMatch != currentTransaction.IDString() { 197 rw.WriteHeader(http.StatusPreconditionFailed) 198 return 199 } 200 } 201 202 f(rw, req) 203 } 204 } 205 206 // attachCurrentTransactionHeader is used to notify 207 func attachCurrentTransactionHeader(r *repository.Repository, rw http.ResponseWriter) { 208 header := rw.Header() 209 210 currentTransaction, err := r.CurrentTransaction() 211 if err != nil && err != repository.ErrTransactionNotExists { 212 errorText(rw, "Internal Error", http.StatusInternalServerError) 213 return 214 } else if currentTransaction != nil { 215 header.Set("X-Current-Transaction-Id", currentTransaction.IDString()) 216 } 217 } 218 219 // ListenAndServe starts serving requests on the default port using TLS 220 func (s *Server) ListenAndServe() error { 221 listener, err := net.Listen("tcp", s.http.Addr) 222 if err != nil { 223 return err 224 } 225 return s.Serve(listener) 226 } 227 228 // loadCertificate loads and parses the on-disk certificates and keeps them 229 // in memory for later use. 230 func (s *Server) loadCertificate() error { 231 var err error 232 s.certificate, err = tls.LoadX509KeyPair(s.certFile, s.keyFile) 233 if err != nil { 234 return err 235 } 236 if len(s.certificate.Certificate) != 1 { 237 return fmt.Errorf("bad number of certificates loaded (%d)", 238 len(s.certificate.Certificate)) 239 } 240 fp, err := s.CertificateFingerprint() 241 Log.Info("loaded certificate", log15.Ctx{"fingerprint": fp}) 242 return err 243 } 244 245 // CertificateFingerprint returns the server's certificate public key fingerprint 246 func (s *Server) CertificateFingerprint() (string, error) { 247 fp, err := x509.CertificateFingerprintFromBytes(s.certificate.Certificate[0]) 248 if err != nil { 249 return "", err 250 } 251 return fp, nil 252 } 253 254 // Serve serves TLS-enabled requests on the given listener. 255 func (s *Server) Serve(l net.Listener) error { 256 config := &tls.Config{ 257 CipherSuites: []uint16{ 258 tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 259 }, 260 MinVersion: tls.VersionTLS12, 261 NextProtos: []string{"http/1.1"}, 262 Certificates: []tls.Certificate{s.certificate}, 263 } 264 tlsListener := tls.NewListener(tcpKeepAliveListener{l.(*net.TCPListener)}, config) 265 return s.http.Serve(tlsListener) 266 }