├── .travis.yml ├── screencasts ├── APIs.gif ├── operations_with_auth.gif └── git_operations_without_auth.gif ├── error.go ├── .gitignore ├── response.go ├── repo.go ├── commit.go ├── LICENSE.md ├── upload_pack.go ├── receive_pack.go ├── service.go ├── api_utils.go ├── server.go ├── basic_auth.go ├── branch.go ├── main.go ├── server_utils.go ├── api_handler.go └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.5 5 | - tip 6 | -------------------------------------------------------------------------------- /screencasts/APIs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikju/servidor/HEAD/screencasts/APIs.gif -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Error struct { 4 | Message string `json:"message"` 5 | } 6 | -------------------------------------------------------------------------------- /screencasts/operations_with_auth.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikju/servidor/HEAD/screencasts/operations_with_auth.gif -------------------------------------------------------------------------------- /screencasts/git_operations_without_auth.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratikju/servidor/HEAD/screencasts/git_operations_without_auth.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Executables 2 | 3 | servidor 4 | 5 | # Repository directory 6 | 7 | repos/ 8 | 9 | # Password and certificate files 10 | 11 | *.txt 12 | *.key 13 | *.pem 14 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type BaseResponse struct { 4 | CreateRepositoryURL string `json:"create_repo_url"` 5 | UserRepositoriesURL string `json:"user_repositories_url"` 6 | UserRepositoryURL string `json:"user_repository_url"` 7 | BranchesURL string `json:"branches_url"` 8 | } 9 | 10 | type CreateResponse struct { 11 | ResponseMessage string `json:"response_message"` 12 | CloneURL string `json:"clone_url"` 13 | } 14 | -------------------------------------------------------------------------------- /repo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Repository struct { 9 | Name string `json:"name"` 10 | CloneURL string `json:"clone_url"` 11 | Owner string `json:"owner"` 12 | BranchesURL string `json:"branches_url"` 13 | } 14 | 15 | func GetRepository(h, u, r string) Repository { 16 | var repo Repository 17 | rawRepoName := strings.Split(r, ".git")[0] 18 | repo = Repository{Name: rawRepoName, 19 | CloneURL: FormCloneURL(h, u, r), 20 | Owner: u, 21 | BranchesURL: fmt.Sprintf("%s/api/%s/repos/%s/branches{/branch-name}", GetProtocol(config.SSLEnabled)+h, u, rawRepoName), 22 | } 23 | return repo 24 | } 25 | -------------------------------------------------------------------------------- /commit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/libgit2/git2go" 5 | ) 6 | 7 | type Commit struct { 8 | Message string `json:"message"` 9 | ID string `json:"id"` 10 | ObjectType string `json:"object_type"` 11 | Author *git.Signature `json:"author"` 12 | } 13 | 14 | func GetCommits(oid *git.Oid, revWalk *git.RevWalk) []Commit { 15 | var commit Commit 16 | var commits []Commit 17 | 18 | err := revWalk.Push(oid) 19 | if err != nil { 20 | return commits 21 | } 22 | f := func(c *git.Commit) bool { 23 | commit = Commit{Message: c.Summary(), ID: c.Id().String(), ObjectType: c.Type().String(), Author: c.Author()} 24 | commits = append(commits, commit) 25 | return true 26 | } 27 | _ = revWalk.Iterate(f) 28 | return commits 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Pratik Singh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /upload_pack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "os/exec" 9 | ) 10 | 11 | func uploadPackHandler(w http.ResponseWriter, r *http.Request) { 12 | userName, repoName, _ := GetParamValues(r) 13 | execPath := RepoPath(userName, repoName) 14 | 15 | cmd := exec.Command(config.GitPath, "upload-pack", "--stateless-rpc", execPath) 16 | stdin, stdout, stderr, ok := GetChildPipes(cmd, w) 17 | if !ok { 18 | return 19 | } 20 | 21 | if err := cmd.Start(); err != nil { 22 | log.Println(err) 23 | http.Error(w, "Error while spawning", http.StatusInternalServerError) 24 | return 25 | } 26 | 27 | reqBody, err := ioutil.ReadAll(r.Body) 28 | if err != nil { 29 | log.Println("Error while reading request body:", err) 30 | http.Error(w, "Error while reading request body", http.StatusInternalServerError) 31 | return 32 | } 33 | stdin.Write(reqBody) 34 | 35 | contentType := "application/x-git-upload-pack-result" 36 | SetHeader(w, contentType) 37 | 38 | go io.Copy(w, stdout) 39 | go io.Copy(w, stderr) 40 | 41 | if err := cmd.Wait(); err != nil { 42 | log.Println("Error while waiting:", err) 43 | return 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /receive_pack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "os/exec" 9 | ) 10 | 11 | func receivePackHandler(w http.ResponseWriter, r *http.Request) { 12 | userName, repoName, _ := GetParamValues(r) 13 | execPath := RepoPath(userName, repoName) 14 | 15 | cmd := exec.Command(config.GitPath, "receive-pack", "--stateless-rpc", execPath) 16 | stdin, stdout, stderr, ok := GetChildPipes(cmd, w) 17 | if !ok { 18 | return 19 | } 20 | 21 | if err := cmd.Start(); err != nil { 22 | log.Println(err) 23 | http.Error(w, "Error while spawning", http.StatusInternalServerError) 24 | return 25 | } 26 | 27 | reqBody, err := ioutil.ReadAll(r.Body) 28 | if err != nil { 29 | log.Println("Error while reading request body:", err) 30 | http.Error(w, "Error while reading request body", http.StatusInternalServerError) 31 | return 32 | } 33 | stdin.Write(reqBody) 34 | 35 | contentType := "application/x-git-receive-pack-result" 36 | SetHeader(w, contentType) 37 | 38 | go io.Copy(w, stdout) 39 | go io.Copy(w, stderr) 40 | 41 | if err := cmd.Wait(); err != nil { 42 | log.Println("Error while waiting:", err) 43 | return 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /service.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | "os/exec" 9 | ) 10 | 11 | func serviceHandler(w http.ResponseWriter, r *http.Request) { 12 | userName, repoName, _ := GetParamValues(r) 13 | service := FindService(r) 14 | if ok := IsRestricted(service); ok { 15 | log.Println("Operation not permitted") 16 | http.Error(w, "Operation not permitted", http.StatusForbidden) 17 | return 18 | } 19 | execPath := RepoPath(userName, repoName) 20 | if ok := IsExistingRepository(execPath); !ok { 21 | log.Println("repository not found") 22 | http.Error(w, "repository not found", http.StatusNotFound) 23 | return 24 | } 25 | 26 | cmd := exec.Command(config.GitPath, service, "--stateless-rpc", "--advertise-refs", execPath) 27 | _, stdout, stderr, ok := GetChildPipes(cmd, w) 28 | if !ok { 29 | return 30 | } 31 | 32 | if err := cmd.Start(); err != nil { 33 | log.Println("Error while spawning:", err) 34 | http.Error(w, "Error while spawning", http.StatusInternalServerError) 35 | return 36 | } 37 | 38 | contentType := fmt.Sprintf("application/x-git-%s-advertisement", service) 39 | SetHeader(w, contentType) 40 | w.Write([]byte(CreateFirstPKTLine(service))) 41 | go io.Copy(w, stdout) 42 | go io.Copy(w, stderr) 43 | if err := cmd.Wait(); err != nil { 44 | log.Println("Error while waiting:", err) 45 | return 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /api_utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | ) 11 | 12 | func FindAllDir(targetPath string) ([]os.FileInfo, bool) { 13 | var list []os.FileInfo 14 | var err error 15 | if list, err = ioutil.ReadDir(targetPath); err != nil { 16 | log.Println("Error finding repository:", err) 17 | return nil, false 18 | } 19 | return list, true 20 | } 21 | 22 | func FormCloneURL(host, userName, repoName string) string { 23 | return (fmt.Sprintf(GetProtocol(config.SSLEnabled) + host + "/" + userName + "/" + repoName)) 24 | } 25 | 26 | func GetRepoCreateURL() string { 27 | return "/api/repos/create" 28 | } 29 | 30 | func GetReposURL() string { 31 | return "/api/{user-name}/repos" 32 | } 33 | 34 | func GetRepoURL() string { 35 | return "/api/{user-name}/repos/{repo-name}" 36 | } 37 | 38 | func GetBranchesURL() string { 39 | return "/api/{user-name}/repos/{repo-name}/branches" 40 | } 41 | 42 | func GetBranchURL() string { 43 | return "/api/{user-name}/repos/{repo-name}/branches/{branch-name}" 44 | } 45 | 46 | func GetProtocol(ssl bool) string { 47 | if ssl { 48 | return "https://" 49 | } 50 | return "http://" 51 | } 52 | 53 | func WriteIndentedJSON(w io.Writer, v interface{}, prefix, indent string) { 54 | resp, _ := json.MarshalIndent(v, prefix, indent) 55 | w.Write(resp) 56 | w.Write([]byte("\n")) 57 | } 58 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gorilla/mux" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | func GitServer() { 11 | host := fmt.Sprintf("%s:%s", config.Hostname, config.Port) 12 | log.Println("Starting git http server at", host) 13 | 14 | r := mux.NewRouter() 15 | attachHandler(r) 16 | 17 | if config.SSLEnabled { 18 | if err := http.ListenAndServeTLS(host, "server.pem", "server.key", r); err != nil { 19 | log.Fatal(err) 20 | } 21 | } else { 22 | if err := http.ListenAndServe(host, r); err != nil { 23 | log.Fatal(err) 24 | } 25 | } 26 | } 27 | 28 | func attachHandler(r *mux.Router) { 29 | //git methods Handler 30 | r.HandleFunc(`/{user-name}/{repo-name:([a-zA-Z0-9\-\.\_]+)}/info/refs`, basicAuthentication(serviceHandler)).Methods("GET") 31 | r.HandleFunc(`/{user-name}/{repo-name:([a-zA-Z0-9\-\.\_]+)}/git-upload-pack`, basicAuthentication(uploadPackHandler)).Methods("POST") 32 | r.HandleFunc(`/{user-name}/{repo-name:([a-zA-Z0-9\-\.\_]+)}/git-receive-pack`, basicAuthentication(receivePackHandler)).Methods("POST") 33 | 34 | //APIs handlers 35 | r.HandleFunc("/", rootHandler).Methods("GET") 36 | r.HandleFunc(GetRepoCreateURL(), basicAuthentication(repoCreateHandler)).Methods("POST") 37 | r.HandleFunc(GetReposURL(), repoIndexHandler).Methods("GET") 38 | r.HandleFunc(GetRepoURL(), repoShowHandler).Methods("GET") 39 | r.HandleFunc(GetBranchesURL(), branchIndexHandler).Methods("GET") 40 | r.HandleFunc(GetBranchURL(), branchShowHandler).Methods("GET") 41 | } 42 | -------------------------------------------------------------------------------- /basic_auth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "crypto/sha1" 6 | "encoding/base64" 7 | "net/http" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func basicAuthentication(reqHandler http.HandlerFunc) http.HandlerFunc { 13 | return func(w http.ResponseWriter, r *http.Request) { 14 | if config.AuthEnabled { 15 | username, password, ok := r.BasicAuth() 16 | if !ok { 17 | renderUnauthorized(w, "Authentication failed - Provide Basic Authentication - username:password") 18 | return 19 | } 20 | if !validate(username, password) { 21 | renderUnauthorized(w, "Authentication failed - incorrect username or password") 22 | return 23 | } 24 | } 25 | reqHandler(w, r) 26 | } 27 | } 28 | 29 | func validate(username, password string) bool { 30 | file, err := os.Open(config.PasswdFilePath) 31 | if err != nil { 32 | return false 33 | } 34 | defer file.Close() 35 | 36 | scanner := bufio.NewScanner(file) 37 | for scanner.Scan() { 38 | params := strings.Split(scanner.Text(), ":") 39 | if username == params[0] && matchPassword(password, params[1]) { 40 | return true 41 | } 42 | } 43 | return false 44 | } 45 | 46 | func matchPassword(savedPwd string, sentPwd string) bool { 47 | hash := sha1.New() 48 | hash.Write([]byte(savedPwd)) 49 | pwdCheck := strings.Replace(base64.URLEncoding.EncodeToString(hash.Sum(nil)), "-", "+", -1) 50 | pwdCheck = strings.Replace(pwdCheck, "_", "/", -1) 51 | 52 | return (pwdCheck == strings.Split(sentPwd, "{SHA}")[1]) 53 | } 54 | 55 | func renderUnauthorized(w http.ResponseWriter, error string) { 56 | w.Header().Set("WWW-Authenticate", "Basic realm=\"\"") 57 | w.WriteHeader(http.StatusUnauthorized) 58 | errJSON := Error{Message: error} 59 | WriteIndentedJSON(w, errJSON, "", " ") 60 | } 61 | -------------------------------------------------------------------------------- /branch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/libgit2/git2go" 5 | "log" 6 | ) 7 | 8 | type Branch struct { 9 | Name string `json:"name"` 10 | IsHead bool `json:"isHead"` 11 | Commits []Commit `json:"commits"` 12 | } 13 | 14 | func GetBranches(repo *git.Repository) ([]Branch, error) { 15 | var branch Branch 16 | var branches []Branch 17 | 18 | itr, _ := repo.NewReferenceIterator() 19 | refs := getReferences(itr) 20 | 21 | revWalk, err := repo.Walk() 22 | if err != nil { 23 | log.Println(err) 24 | return branches, err 25 | } 26 | 27 | for i := 0; i < len(refs); i++ { 28 | branch = getBranch(refs[i], revWalk) 29 | branches = append(branches, branch) 30 | } 31 | 32 | return branches, nil 33 | } 34 | 35 | func getReferences(itr *git.ReferenceIterator) []*git.Reference { 36 | var ref *git.Reference 37 | var refs []*git.Reference 38 | var err error 39 | for { 40 | ref, err = itr.Next() 41 | if err != nil { 42 | break 43 | } 44 | refs = append(refs, ref) 45 | } 46 | return refs 47 | } 48 | 49 | func getBranch(ref *git.Reference, revWalk *git.RevWalk) Branch { 50 | var branch Branch 51 | b := ref.Branch() 52 | name, err := b.Name() 53 | if err != nil { 54 | log.Println(err) 55 | } 56 | isHead, err := b.IsHead() 57 | if err != nil { 58 | log.Println(err) 59 | } 60 | commits := GetCommits(ref.Target(), revWalk) 61 | branch = Branch{Name: name, IsHead: isHead, Commits: commits} 62 | return branch 63 | } 64 | 65 | func GetBranchByName(name string, repo *git.Repository) (Branch, bool) { 66 | var branch Branch 67 | gitBranch, err := repo.LookupBranch(name, git.BranchLocal) 68 | if err != nil { 69 | return branch, false 70 | } 71 | 72 | revWalk, err := repo.Walk() 73 | if err != nil { 74 | log.Println(err) 75 | return branch, false 76 | } 77 | return getBranch(gitBranch.Reference, revWalk), true 78 | } 79 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type Config struct { 11 | Port string 12 | Hostname string 13 | GitPath string 14 | ReposRootPath string 15 | AuthEnabled bool 16 | PasswdFilePath string 17 | SSLEnabled bool 18 | RestrictReceivePack bool 19 | RestrictUploadPack bool 20 | } 21 | 22 | var ( 23 | port = flag.String("p", "8000", "Port on which servidor will listen") 24 | hostName = flag.String("b", "0.0.0.0", "Hostname to be used") 25 | repo = flag.String("r", GetDefaultReposPath(), "Set the path where repositories will be saved, Just mention the base path(\"repos\" directory will be automatically created inside it)") 26 | gitPath = flag.String("g", GetDefaultGitPath(), "Mention the gitPath if its different on hosting machine") 27 | passwdFile = flag.String("c", "", "Set the path from where the password file is to be read(to be set whenever -a flag is used)") 28 | auth = flag.Bool("a", false, "Enable basic authentication for all http operations") 29 | ssl = flag.Bool("s", false, "Enable tls connection") 30 | restrictPush = flag.Bool("R", false, "Set Whether ReceivePack(push operation) will be restricted") 31 | restrictPull = flag.Bool("U", false, "Set Whether UploadPack(clone, pull, fetch operations) will be restricted") 32 | config Config 33 | ) 34 | 35 | func main() { 36 | flag.Parse() 37 | if *auth { 38 | if *passwdFile == "" { 39 | log.Println("Improper usage") 40 | flag.Usage() 41 | return 42 | } 43 | if _, err := os.Open(*passwdFile); err != nil { 44 | log.Fatal(err) 45 | } 46 | } 47 | config = Config{Port: *port, Hostname: *hostName, ReposRootPath: filepath.Join(*repo, "repos"), 48 | GitPath: *gitPath, AuthEnabled: *auth, PasswdFilePath: *passwdFile, SSLEnabled: *ssl, 49 | RestrictReceivePack: *restrictPush, RestrictUploadPack: *restrictPull} 50 | GitServer() 51 | } 52 | -------------------------------------------------------------------------------- /server_utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gorilla/mux" 6 | "io" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | func GetParamValues(r *http.Request) (string, string, string) { 17 | vars := mux.Vars(r) 18 | userName := vars["user-name"] 19 | repoName := vars["repo-name"] 20 | branchName := vars["branch-name"] 21 | return userName, repoName, branchName 22 | } 23 | 24 | func FindService(r *http.Request) string { 25 | s := r.URL.Query().Get("service") 26 | service := strings.SplitN(s, "-", 2)[1] 27 | return service 28 | } 29 | 30 | func SetHeader(w http.ResponseWriter, contentType string) { 31 | w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT") 32 | w.Header().Set("Pragma", "no-cache") 33 | w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate") 34 | w.Header().Set("Content-Type", contentType) 35 | w.WriteHeader(http.StatusOK) 36 | } 37 | 38 | func CreateFirstPKTLine(service string) string { 39 | packet := fmt.Sprintf("# service=git-%s\n", service) 40 | 41 | prefix := strconv.FormatInt(int64(len(packet)+4), 16) 42 | if len(prefix)%4 != 0 { 43 | prefix = strings.Repeat("0", 4-len(prefix)%4) + prefix 44 | } 45 | magicMarker := "0000" 46 | return prefix + packet + magicMarker 47 | } 48 | 49 | func GetDefaultReposPath() string { 50 | rPath, _ := os.Getwd() 51 | return rPath 52 | } 53 | 54 | func IsExistingRepository(path string) bool { 55 | f, e := os.Stat(path) 56 | if e != nil { 57 | return false 58 | } 59 | return f.IsDir() 60 | } 61 | 62 | func IsRestricted(service string) bool { 63 | if service == "receive-pack" { 64 | return config.RestrictReceivePack 65 | } 66 | if service == "upload-pack" { 67 | return config.RestrictUploadPack 68 | } 69 | return true 70 | } 71 | 72 | func UserPath(userName string) string { 73 | return filepath.Join(config.ReposRootPath, strings.ToLower(userName)) 74 | } 75 | 76 | func RepoPath(userName, repoName string) string { 77 | return filepath.Join(UserPath(userName), FormatRepoName(repoName)) 78 | } 79 | 80 | func FormatRepoName(repoName string) string { 81 | var r string 82 | if strings.HasSuffix(repoName, ".git") { 83 | r = strings.ToLower(repoName) 84 | } else { 85 | r = strings.ToLower(repoName) + ".git" 86 | } 87 | return r 88 | } 89 | 90 | func GetDefaultGitPath() string { 91 | return "/usr/bin/git" 92 | } 93 | 94 | func GetChildPipes(cmd *exec.Cmd, w http.ResponseWriter) (stdin io.WriteCloser, stdout, stderr io.ReadCloser, ok bool) { 95 | var err error 96 | stdin, err = cmd.StdinPipe() 97 | if err != nil { 98 | log.Println("Error with child stdin pipe:", err) 99 | http.Error(w, "Error with child stdin pipe:", http.StatusInternalServerError) 100 | return 101 | } 102 | stdout, err = cmd.StdoutPipe() 103 | if err != nil { 104 | log.Println("Error with child stdout pipe:", err) 105 | http.Error(w, "Error with child stdout pipe:", http.StatusInternalServerError) 106 | return 107 | } 108 | stderr, err = cmd.StderrPipe() 109 | if err != nil { 110 | log.Println("Error with child stderr pipe:", err) 111 | http.Error(w, "Error with child stderr pipe:", http.StatusInternalServerError) 112 | return 113 | } 114 | return stdin, stdout, stderr, true 115 | } 116 | -------------------------------------------------------------------------------- /api_handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/libgit2/git2go" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | ) 12 | 13 | type Payload struct { 14 | Username string 15 | RepoName string 16 | } 17 | 18 | func rootHandler(w http.ResponseWriter, r *http.Request) { 19 | baseResp := BaseResponse{ 20 | CreateRepositoryURL: fmt.Sprintf(GetProtocol(config.SSLEnabled) + r.Host + GetRepoCreateURL()), 21 | UserRepositoriesURL: fmt.Sprintf(GetProtocol(config.SSLEnabled) + r.Host + GetReposURL()), 22 | UserRepositoryURL: fmt.Sprintf(GetProtocol(config.SSLEnabled) + r.Host + GetRepoURL()), 23 | BranchesURL: fmt.Sprintf(GetProtocol(config.SSLEnabled) + r.Host + GetBranchesURL() + "{/branch-name}"), 24 | } 25 | 26 | WriteIndentedJSON(w, baseResp, "", " ") 27 | } 28 | 29 | func repoCreateHandler(w http.ResponseWriter, r *http.Request) { 30 | var resp CreateResponse 31 | resp.ResponseMessage = "Unknown error. Follow README" 32 | resp.CloneURL = "" 33 | 34 | wd, _ := os.Getwd() 35 | 36 | defer func() { 37 | WriteIndentedJSON(w, resp, "", " ") 38 | if err := os.Chdir(wd); err != nil { 39 | log.Println(err) 40 | } 41 | }() 42 | 43 | var payload Payload 44 | decoder := json.NewDecoder(r.Body) 45 | if err := decoder.Decode(&payload); err != nil { 46 | log.Println(err) 47 | return 48 | } 49 | if payload.Username == "" || payload.RepoName == "" { 50 | log.Println("Empty username or reponame") 51 | return 52 | } 53 | 54 | usrPath := UserPath(payload.Username) 55 | bareRepo := FormatRepoName(payload.RepoName) 56 | url := FormCloneURL(r.Host, payload.Username, bareRepo) 57 | 58 | if ok := IsExistingRepository(RepoPath(payload.Username, payload.RepoName)); ok { 59 | resp.ResponseMessage = fmt.Sprintf("repository already exists for %s", payload.Username) 60 | resp.CloneURL = url 61 | return 62 | } 63 | 64 | if err := os.MkdirAll(usrPath, 0775); err != nil { 65 | resp.ResponseMessage = "error while creating user" 66 | return 67 | } 68 | 69 | if err := os.Chdir(usrPath); err != nil { 70 | resp.ResponseMessage = "error while creating new repository" 71 | return 72 | } 73 | 74 | cmd := exec.Command(config.GitPath, "init", "--bare", bareRepo) 75 | 76 | if err := cmd.Start(); err == nil { 77 | resp.CloneURL = url 78 | resp.ResponseMessage = "repository created successfully" 79 | } else { 80 | resp.ResponseMessage = "error while creating new repository" 81 | return 82 | } 83 | if err := cmd.Wait(); err != nil { 84 | log.Println("Error while waiting:", err) 85 | return 86 | } 87 | } 88 | 89 | func repoIndexHandler(w http.ResponseWriter, r *http.Request) { 90 | userName, _, _ := GetParamValues(r) 91 | var errJSON Error 92 | list, ok := FindAllDir(UserPath(userName)) 93 | if !ok { 94 | errJSON = Error{Message: "repository not found"} 95 | WriteIndentedJSON(w, errJSON, "", " ") 96 | return 97 | } 98 | var repo Repository 99 | var repos []Repository 100 | 101 | for i := 0; i < len(list); i++ { 102 | repo = GetRepository(r.Host, userName, list[i].Name()) 103 | repos = append(repos, repo) 104 | } 105 | WriteIndentedJSON(w, repos, "", " ") 106 | } 107 | 108 | func repoShowHandler(w http.ResponseWriter, r *http.Request) { 109 | var errJSON Error 110 | userName, repoName, _ := GetParamValues(r) 111 | if ok := IsExistingRepository(RepoPath(userName, repoName)); !ok { 112 | errJSON = Error{Message: "repository not found"} 113 | WriteIndentedJSON(w, errJSON, "", " ") 114 | return 115 | } 116 | repo := GetRepository(r.Host, userName, FormatRepoName(repoName)) 117 | WriteIndentedJSON(w, repo, "", " ") 118 | } 119 | 120 | func branchIndexHandler(w http.ResponseWriter, r *http.Request) { 121 | var errJSON Error 122 | userName, repoName, _ := GetParamValues(r) 123 | if ok := IsExistingRepository(RepoPath(userName, repoName)); !ok { 124 | errJSON = Error{Message: "repository not found"} 125 | WriteIndentedJSON(w, errJSON, "", " ") 126 | return 127 | } 128 | re, _ := git.OpenRepository(RepoPath(userName, repoName)) 129 | branches, _ := GetBranches(re) 130 | WriteIndentedJSON(w, branches, "", " ") 131 | } 132 | 133 | func branchShowHandler(w http.ResponseWriter, r *http.Request) { 134 | var errJSON Error 135 | userName, repoName, branchName := GetParamValues(r) 136 | if ok := IsExistingRepository(RepoPath(userName, repoName)); !ok { 137 | errJSON = Error{Message: "repository not found"} 138 | WriteIndentedJSON(w, errJSON, "", " ") 139 | return 140 | } 141 | 142 | re, _ := git.OpenRepository(RepoPath(userName, repoName)) 143 | branch, ok := GetBranchByName(branchName, re) 144 | if !ok { 145 | errJSON = Error{Message: "branch not found"} 146 | WriteIndentedJSON(w, errJSON, "", " ") 147 | return 148 | } 149 | 150 | WriteIndentedJSON(w, branch, "", " ") 151 | } 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **Servidor** 2 | 3 | Servidor is a light-weight no-database git http server following git smart HTTP protocol. You can do all kind of git remote operations like push, pull, fetch and clone. Host the server very easily and get started. 4 | 5 | ## Features supported as of now 6 | 7 | - Git Remote Operations 8 | - [x] Cloning of git repository. 9 | - [x] Push operation. 10 | - [x] Fetch operation. 11 | - [x] Pull operation. 12 | - APIs 13 | - [x] Create git repository - POST 14 | - [x] List all repositories corresponding to a user. - GET 15 | - [x] List a particular repository corresponding to a user. -GET 16 | - [x] List all the branches in a repository. - GET 17 | - [x] List a particular branch in a repository. - GET 18 | - Extra Features 19 | - [x] Basic authentication as per flag 20 | - [x] Allowing TLS connection as per flag. 21 | - [x] Restricting push, pull operations as per flag. 22 | 23 | ## Demo 24 | 25 | ![](https://github.com/gophergala2016/servidor/blob/master/screencasts/git_operations_without_auth.gif) 26 | 27 | [More screencasts](https://github.com/gophergala2016/servidor/tree/master/screencasts) 28 | 29 | ## Motivation 30 | 31 | While setting up our project a few months back, we had to go through the trouble of setup and configuration 32 | needed in GitLab. To do away with all of that in future, I decided to create a git server of my own. It will typically help small group of coders, who wish to maintain private repositories within a local network and don't want to do all sorts of setup needed in GitLab and other providers. 33 | 34 | ## Installation 35 | 36 | - Install cmake from brew(Mac) or apt-get(linux) package manager. To build from source, follow [this](https://cmake.org/install/) link 37 | 38 | - Install libgit2 as follows : 39 | ``` 40 | $ wget https://github.com/libgit2/libgit2/archive/v0.23.4.tar.gz 41 | $ tar xzf v0.23.4.tar.gz 42 | $ cd libgit2-0.23.4/ 43 | $ cmake . 44 | $ make 45 | $ sudo make install 46 | ``` 47 | 48 | - Build the project 49 | 50 | Assuming you have installed a recent version of 51 | [Go](https://golang.org/doc/install), you can simply run 52 | 53 | ``` 54 | go get github.com/gophergala2016/servidor 55 | ``` 56 | 57 | This will download Servidor to `$GOPATH/src/github.com/gophergala2016/servidor`. From 58 | this directory run `go build` to create the `servidor` binary. 59 | 60 | - Troubleshooting:- 61 | ```ImportError: libgit2.so.0: cannot open shared object file: No such file or directory``` 62 | This happens for instance in Ubuntu, the libgit2 library is installed within the `/usr/local/lib` directory, but the linker does not look for it there. 63 | To fix this call 64 | ``` 65 | $ sudo ldconfig 66 | ``` 67 | 68 | ## Getting started 69 | 70 | Start the server by executing `servidor` binary. By default, servidor will listen to http://localhost:8000 for incoming requests. 71 | 72 | 73 | ## Options: 74 | ``` 75 | ./servidor -h 76 | Usage of ./servidor: 77 | -R Set Whether ReceivePack(push operation) will be restricted 78 | -U Set Whether UploadPack(clone, pull, fetch operations) will be restricted 79 | -a Enable basic authentication for all http operations 80 | -b string 81 | Hostname to be used (default "0.0.0.0") 82 | -c string 83 | Set the path from where the password file is to be read(to be set whenever -a flag is used) 84 | -g string 85 | Mention the gitPath if its different on hosting machine (default "/usr/bin/git") 86 | -p string 87 | Port on which servidor will listen (default "8000") 88 | -r string 89 | Set the path where repositories will be saved, Just mention the base path("repos" directory will be automatically created inside it) (default "/home/administrator/servidor") 90 | -s Enable tls connection 91 | ``` 92 | 93 | ## Usages: 94 | 95 | - Create git repository as follows : 96 | 97 | ``` 98 | $ curl -X POST http://:/api/repos/create 99 | -d '{"username":"username1","reponame":"project1"}' 100 | ``` 101 | 102 | - Typical successful response : 103 | ``` 104 | { 105 | "response_message": "repository created successfully", 106 | "clone_url": "http://:/username1/project1.git" 107 | } 108 | ``` 109 | 110 | - Typical unsuccessful response : 111 | ``` 112 | { 113 | "response_message": "repository already exists for user", 114 | "clone_url": "http://:/username1/project1.git" 115 | } 116 | ``` 117 | 118 | - Now, Clone the repository using the clone_url. Do stuffs, push changes set to remote, pull changes from remote etc. 119 | 120 | ## Setup for extra features 121 | 122 | - To enable basic authentication, create the password file as follows 123 | 124 | *The password file has entries of the format : ```username:password(in SHA-1 encoded format)```* 125 | 126 | - To generate the password file, use htpasswd tool. Install it by using 127 | ``` 128 | $ sudo apt-get install apache2-utils 129 | ``` 130 | 131 | - Once installed you can use 132 | ``` 133 | $ htpasswd -cs path/to/create/password/file/filename username1 134 | 135 | $ htpasswd -s path/to/create/password/file/filename username2 136 | ``` 137 | 138 | Note: while creating sha-1 password for the second user, do not use -c flag. It is used to create the file for the first time. See [documentation](https://httpd.apache.org/docs/2.2/programs/htpasswd.html) if needed. 139 | 140 | - To enable ssl connection 141 | 142 | *Generated private key* 143 | ``` 144 | $ openssl genrsa -out server.key 2048 145 | ``` 146 | 147 | *Generate the certificate* 148 | ``` 149 | $ openssl req -new -x509 -key server.key -out server.pem -days 3650 150 | ``` 151 | 152 | Since the certificates are self authorized, server verification must be turned off for clients: 153 | 154 | For curl, `use -k flag` 155 | 156 | For git, `export GIT_SSL_NO_VERIFY=1` 157 | 158 | ## API References 159 | 160 | - To display the list of APIs available 161 | ``` 162 | $ curl http://: 163 | ``` 164 | *Response:* 165 | 166 | ``` 167 | { 168 | "create_repo_url": "http://:/api/repos/create", 169 | "user_repositories_url": "http://:/api/{user-name}/repos", 170 | "user_repository_url": "http://:/api/{user-name}/repos/{repo-name}", 171 | "branches_url": "http://:/api/{user-name}/repos/{repo-name}/branches{/branch-name}" 172 | } 173 | ``` 174 | 175 | ## Feature to come 176 | 177 | - Webhook support 178 | - More repo metrics 179 | 180 | ## Libraries Used 181 | - [git2go](https://github.com/libgit2/git2go) 182 | - [gorilla mux](https://github.com/gorilla/mux) 183 | 184 | ## License 185 | 186 | MIT, see the LICENSE file. 187 | --------------------------------------------------------------------------------