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  }