package main import ( "encoding/json" "flag" "fmt" "log" "net/http" "regexp" "strings" "time" "gitlab.com/mporrato/uBrowserSync/syncstore" ) const ( apiVersion = "1.1.13" infoMessage = "Powered by uBrowserSync" defaultMaxSyncSize = 512000 ) var ( store syncstore.Store maxSyncSize = defaultMaxSyncSize listen string serviceStatus = 1 sidRe = regexp.MustCompile("^[[:xdigit:]]{32}$") invalidRequestError = syncstore.NewSyncError( "Invalid request", "Malformed request body", http.StatusBadRequest) ) func sendJSON(w http.ResponseWriter, status int, data interface{}) { body, err := json.Marshal(data) if err != nil { log.Printf("sendJSON(%v): json.Marshal() failed: %v", data, err) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _, err = w.Write(body) if err != nil { log.Println("sendJSON() failed:", err) } } func sendJSONOk(w http.ResponseWriter, data interface{}) { sendJSON(w, http.StatusOK, data) } func sendJSONError(w http.ResponseWriter, err error) { switch e := err.(type) { case syncstore.SyncError: sendJSON(w, e.StatusCode, e.Payload) default: sendJSON(w, http.StatusInternalServerError, syncstore.ErrorPayload{ Code: "Internal server error", Message: e.Error()}) } } func info(w http.ResponseWriter, req *http.Request) { if req.Method != "GET" || req.URL.Path != "/info" { sendJSONError(w, syncstore.NotImplementedError) } else { log.Println("info()") serviceInfo := syncstore.ServiceInfoResp{ Version: apiVersion, Message: infoMessage, Status: serviceStatus, MaxSyncSize: maxSyncSize} sendJSONOk(w, serviceInfo) } } func createSync(w http.ResponseWriter, req *http.Request) { body := new(syncstore.CreateReq) req.Body = http.MaxBytesReader(w, req.Body, 10000) err := json.NewDecoder(req.Body).Decode(&body) if err != nil { sendJSONError(w, invalidRequestError) return } if len(body.Version) > 20 { sendJSONError(w, invalidRequestError) return } resp, err := store.Create(body.Version) if err != nil { sendJSONError(w, err) return } sendJSONOk(w, resp) } func getSync(syncId string, w http.ResponseWriter, _ *http.Request) { resp, err := store.Get(syncId) if err != nil { sendJSONError(w, err) return } sendJSONOk(w, resp) } func getLastUpdated(syncId string, w http.ResponseWriter, _ *http.Request) { resp, err := store.GetLastUpdated(syncId) if err != nil { sendJSONError(w, err) return } sendJSONOk(w, syncstore.LastUpdatedResp{LastUpdated: resp}) } func getVersion(syncId string, w http.ResponseWriter, _ *http.Request) { resp, err := store.GetVersion(syncId) if err != nil { sendJSONError(w, err) return } sendJSONOk(w, syncstore.GetSyncVerResp{Version: resp}) } func updateSync(syncId string, w http.ResponseWriter, req *http.Request) { body := new(syncstore.UpdateReq) req.Body = http.MaxBytesReader(w, req.Body, int64(10000 + maxSyncSize)) err := json.NewDecoder(req.Body).Decode(&body) if err != nil { sendJSONError(w, invalidRequestError) return } if len(body.Bookmarks) > maxSyncSize { sendJSONError(w, syncstore.SyncDataLimitExceededError) return } resp, err := store.Update(syncId, body.Bookmarks, body.LastUpdated) if err != nil { sendJSONError(w, err) return } sendJSONOk(w, syncstore.UpdateResp{LastUpdated: resp}) } func bookmarks(w http.ResponseWriter, req *http.Request) { elements := strings.Split(strings.Trim(req.URL.Path, "/"), "/") if len(elements) == 1 && req.Method == "POST" { log.Println("createSync()") createSync(w, req) return } syncId := elements[1] if !sidRe.MatchString(elements[1]) { sendJSONError(w, syncstore.NotImplementedError) return } if len(elements) == 2 { if req.Method == "GET" { log.Printf("getSync(%s)", syncId) getSync(syncId, w, req) return } if req.Method == "PUT" { log.Printf("updateSync(%s)", syncId) updateSync(syncId, w, req) return } // TODO: Handle HEAD requests sendJSONError(w, syncstore.MethodNotImplementedError) return } if len(elements) == 3 { if elements[2] == "lastUpdated" { if req.Method == "GET" { log.Printf("getLastUpdated(%s)", syncId) getLastUpdated(syncId, w, req) return } sendJSONError(w, syncstore.MethodNotImplementedError) return } if elements[2] == "version" { if req.Method == "GET" { log.Printf("getVersion(%s)", syncId) getVersion(syncId, w, req) return } sendJSONError(w, syncstore.MethodNotImplementedError) return } } sendJSONError(w, syncstore.NotImplementedError) } func notFound(w http.ResponseWriter, _ *http.Request) { sendJSONError(w, syncstore.NotImplementedError) } func init() { var ( err error storeFlag string storeDrv syncstore.StoreDriver ) flag.StringVar(&listen, "listen", ":8090", "listen address and port") flag.StringVar(&storeFlag, "store", "fs:data", "blob store driver") flag.IntVar(&maxSyncSize, "maxsize", defaultMaxSyncSize, "maximum size of a sync in bytes") flag.Parse() storeFlagTokens := strings.Split(storeFlag, ":") switch storeFlagTokens[0] { case "fs": if len(storeFlagTokens) != 2 { err = fmt.Errorf("argument required for fs store driver") } else { storeDrv, err = syncstore.NewFSStore(storeFlagTokens[1]) } case "mem": storeDrv, err = syncstore.NewMemStore() default: err = fmt.Errorf("Invalid store driver: " + storeFlagTokens[0]) } if err != nil { log.Fatalf("store initialization failed: %v", err) } store = syncstore.NewStore(storeDrv) } func main() { mux := http.NewServeMux() mux.HandleFunc("/", notFound) mux.HandleFunc("/info", info) mux.HandleFunc("/info/", info) mux.HandleFunc("/bookmarks", bookmarks) mux.HandleFunc("/bookmarks/", bookmarks) log.Println("HTTP server listening on", listen) server := &http.Server{ Addr: listen, Handler: mux, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, MaxHeaderBytes: 5000} err := server.ListenAndServe() log.Println("HTTP server terminated", err) }