diff --git a/webapi/API.go b/webapi/API.go index 7212b67..fd70180 100644 --- a/webapi/API.go +++ b/webapi/API.go @@ -7,191 +7,192 @@ Author: Peter Kleissner package webapi import ( - "crypto/tls" - "encoding/json" - "errors" - "net/http" - "sync" - "time" + "crypto/tls" + "encoding/json" + "errors" + "net/http" + "sync" + "time" - "github.com/IncSW/geoip2" - "github.com/PeernetOfficial/core" - "github.com/google/uuid" - "github.com/gorilla/mux" - "github.com/gorilla/websocket" + "github.com/IncSW/geoip2" + "github.com/PeernetOfficial/core" + "github.com/google/uuid" + "github.com/gorilla/mux" + "github.com/gorilla/websocket" ) type WebapiInstance struct { - Backend *core.Backend - geoipCityReader *geoip2.CityReader + backend *core.Backend + geoipCityReader *geoip2.CityReader - // Router can be used to register additional API functions - Router *mux.Router - AllowKeyInParam []string // List of paths that accept the API key as &k= parameter + // Router can be used to register additional API functions + Router *mux.Router + AllowKeyInParam []string // List of paths that accept the API key as &k= parameter - // search jobs - allJobs map[uuid.UUID]*SearchJob - allJobsMutex sync.RWMutex + // search jobs + allJobs map[uuid.UUID]*SearchJob + allJobsMutex sync.RWMutex - // download info - downloads map[uuid.UUID]*downloadInfo - downloadsMutex sync.RWMutex + // download info + downloads map[uuid.UUID]*downloadInfo + downloadsMutex sync.RWMutex } // WSUpgrader is used for websocket functionality. It allows all requests. var WSUpgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - CheckOrigin: func(r *http.Request) bool { - // allow all connections by default - return true - }, + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + // allow all connections by default + return true + }, } // Start starts the API. ListenAddresses is a list of IP:Ports. // The certificate file and key are only used if SSL is enabled. The read and write timeout may be 0 for no timeout. // The API key may be uuid.Nil to disable it although this is not recommended for security reasons. func Start(Backend *core.Backend, ListenAddresses []string, UseSSL bool, CertificateFile, CertificateKey string, TimeoutRead, TimeoutWrite time.Duration, APIKey uuid.UUID) (api *WebapiInstance) { - if len(ListenAddresses) == 0 { - return nil - } + if len(ListenAddresses) == 0 { + return nil + } - api = &WebapiInstance{ - Backend: Backend, - Router: mux.NewRouter(), - AllowKeyInParam: []string{"/file/read", "/file/view"}, - allJobs: make(map[uuid.UUID]*SearchJob), - downloads: make(map[uuid.UUID]*downloadInfo), - } + api = &WebapiInstance{ + backend: Backend, + Router: mux.NewRouter(), + AllowKeyInParam: []string{"/file/read", "/file/view"}, + allJobs: make(map[uuid.UUID]*SearchJob), + downloads: make(map[uuid.UUID]*downloadInfo), + } - if APIKey != uuid.Nil { - api.Router.Use(api.authenticateMiddleware(APIKey)) - } + if APIKey != uuid.Nil { + api.Router.Use(api.authenticateMiddleware(APIKey)) + } - api.Router.HandleFunc("/test", apiTest).Methods("GET") - api.Router.HandleFunc("/status", api.apiStatus).Methods("GET") - api.Router.HandleFunc("/status/peers", api.apiStatusPeers).Methods("GET") - api.Router.HandleFunc("/account/info", api.apiAccountInfo).Methods("GET") - api.Router.HandleFunc("/account/delete", api.apiAccountDelete).Methods("GET") - api.Router.HandleFunc("/blockchain/header", api.apiBlockchainHeaderFunc).Methods("GET") - api.Router.HandleFunc("/blockchain/append", api.apiBlockchainAppend).Methods("POST") - api.Router.HandleFunc("/blockchain/read", api.apiBlockchainRead).Methods("GET") - api.Router.HandleFunc("/blockchain/file/add", api.apiBlockchainFileAdd).Methods("POST") - api.Router.HandleFunc("/blockchain/file/list", api.apiBlockchainFileList).Methods("GET") - api.Router.HandleFunc("/blockchain/file/delete", api.apiBlockchainFileDelete).Methods("POST") - api.Router.HandleFunc("/blockchain/file/update", api.apiBlockchainFileUpdate).Methods("POST") - api.Router.HandleFunc("/profile/list", api.apiProfileList).Methods("GET") - api.Router.HandleFunc("/profile/read", api.apiProfileRead).Methods("GET") - api.Router.HandleFunc("/profile/write", api.apiProfileWrite).Methods("POST") - api.Router.HandleFunc("/profile/delete", api.apiProfileDelete).Methods("POST") - api.Router.HandleFunc("/search", api.apiSearch).Methods("POST") - api.Router.HandleFunc("/search/result", api.apiSearchResult).Methods("GET") - api.Router.HandleFunc("/search/result/ws", api.apiSearchResultStream).Methods("GET") - api.Router.HandleFunc("/search/statistic", api.apiSearchStatistic).Methods("GET") - api.Router.HandleFunc("/search/terminate", api.apiSearchTerminate).Methods("GET") - api.Router.HandleFunc("/explore", api.apiExplore).Methods("GET") - api.Router.HandleFunc("/file/format", api.apiFileFormat).Methods("GET") - api.Router.HandleFunc("/download/start", api.apiDownloadStart).Methods("GET") - api.Router.HandleFunc("/download/status", api.apiDownloadStatus).Methods("GET") - api.Router.HandleFunc("/download/action", api.apiDownloadAction).Methods("GET") - api.Router.HandleFunc("/warehouse/create", api.apiWarehouseCreateFile).Methods("POST") - api.Router.HandleFunc("/warehouse/create/path", api.apiWarehouseCreateFilePath).Methods("GET") - api.Router.HandleFunc("/warehouse/read", api.apiWarehouseReadFile).Methods("GET") - api.Router.HandleFunc("/warehouse/read/path", api.apiWarehouseReadFilePath).Methods("GET") - api.Router.HandleFunc("/warehouse/delete", api.apiWarehouseDeleteFile).Methods("GET") - api.Router.HandleFunc("/file/read", api.apiFileRead).Methods("GET") - api.Router.HandleFunc("/file/view", api.apiFileView).Methods("GET") + api.Router.HandleFunc("/test", apiTest).Methods("GET") + api.Router.HandleFunc("/status", api.apiStatus).Methods("GET") + api.Router.HandleFunc("/status/peers", api.apiStatusPeers).Methods("GET") + api.Router.HandleFunc("/account/info", api.apiAccountInfo).Methods("GET") + api.Router.HandleFunc("/account/delete", api.apiAccountDelete).Methods("GET") + api.Router.HandleFunc("/blockchain/header", api.apiBlockchainHeaderFunc).Methods("GET") + api.Router.HandleFunc("/blockchain/append", api.apiBlockchainAppend).Methods("POST") + api.Router.HandleFunc("/blockchain/read", api.apiBlockchainRead).Methods("GET") + api.Router.HandleFunc("/blockchain/file/add", api.apiBlockchainFileAdd).Methods("POST") + api.Router.HandleFunc("/blockchain/file/list", api.apiBlockchainFileList).Methods("GET") + api.Router.HandleFunc("/blockchain/file/delete", api.apiBlockchainFileDelete).Methods("POST") + api.Router.HandleFunc("/blockchain/file/update", api.apiBlockchainFileUpdate).Methods("POST") + api.Router.HandleFunc("/profile/list", api.apiProfileList).Methods("GET") + api.Router.HandleFunc("/profile/read", api.apiProfileRead).Methods("GET") + api.Router.HandleFunc("/profile/write", api.apiProfileWrite).Methods("POST") + api.Router.HandleFunc("/profile/delete", api.apiProfileDelete).Methods("POST") + api.Router.HandleFunc("/search", api.apiSearch).Methods("POST") + api.Router.HandleFunc("/search/result", api.apiSearchResult).Methods("GET") + api.Router.HandleFunc("/search/result/ws", api.apiSearchResultStream).Methods("GET") + api.Router.HandleFunc("/search/statistic", api.apiSearchStatistic).Methods("GET") + api.Router.HandleFunc("/search/terminate", api.apiSearchTerminate).Methods("GET") + api.Router.HandleFunc("/explore", api.apiExplore).Methods("GET") + api.Router.HandleFunc("/explore/node", api.apiExploreNodeID).Methods("GET") + api.Router.HandleFunc("/file/format", api.apiFileFormat).Methods("GET") + api.Router.HandleFunc("/download/start", api.apiDownloadStart).Methods("GET") + api.Router.HandleFunc("/download/status", api.apiDownloadStatus).Methods("GET") + api.Router.HandleFunc("/download/action", api.apiDownloadAction).Methods("GET") + api.Router.HandleFunc("/warehouse/create", api.apiWarehouseCreateFile).Methods("POST") + api.Router.HandleFunc("/warehouse/create/path", api.apiWarehouseCreateFilePath).Methods("GET") + api.Router.HandleFunc("/warehouse/read", api.apiWarehouseReadFile).Methods("GET") + api.Router.HandleFunc("/warehouse/read/path", api.apiWarehouseReadFilePath).Methods("GET") + api.Router.HandleFunc("/warehouse/delete", api.apiWarehouseDeleteFile).Methods("GET") + api.Router.HandleFunc("/file/read", api.apiFileRead).Methods("GET") + api.Router.HandleFunc("/file/view", api.apiFileView).Methods("GET") - for _, listen := range ListenAddresses { - go startWebAPI(Backend, listen, UseSSL, CertificateFile, CertificateKey, api.Router, "API", TimeoutRead, TimeoutWrite) - } + for _, listen := range ListenAddresses { + go startWebAPI(Backend, listen, UseSSL, CertificateFile, CertificateKey, api.Router, "API", TimeoutRead, TimeoutWrite) + } - return api + return api } // startWebAPI starts a web-server with given parameters and logs the status. If may block forever and only returns if there is an error. // The certificate file and key are only used if SSL is enabled. The read and write timeout may be 0 for no timeout. func startWebAPI(Backend *core.Backend, WebListen string, UseSSL bool, CertificateFile, CertificateKey string, Handler http.Handler, Info string, ReadTimeout, WriteTimeout time.Duration) { - Backend.LogError("startWebAPI", "Start API at '%s'\n", WebListen) + Backend.LogError("startWebAPI", "Start API at '%s'\n", WebListen) - tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} // for security reasons disable TLS 1.0/1.1 + tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} // for security reasons disable TLS 1.0/1.1 - server := &http.Server{ - Addr: WebListen, - Handler: Handler, - ReadTimeout: ReadTimeout, // ReadTimeout is the maximum duration for reading the entire request, including the body. - WriteTimeout: WriteTimeout, // WriteTimeout is the maximum duration before timing out writes of the response. This includes processing time and is therefore the max time any HTTP function may take. - //IdleTimeout: IdleTimeout, // IdleTimeout is the maximum amount of time to wait for the next request when keep-alives are enabled. - TLSConfig: tlsConfig, - } + server := &http.Server{ + Addr: WebListen, + Handler: Handler, + ReadTimeout: ReadTimeout, // ReadTimeout is the maximum duration for reading the entire request, including the body. + WriteTimeout: WriteTimeout, // WriteTimeout is the maximum duration before timing out writes of the response. This includes processing time and is therefore the max time any HTTP function may take. + //IdleTimeout: IdleTimeout, // IdleTimeout is the maximum amount of time to wait for the next request when keep-alives are enabled. + TLSConfig: tlsConfig, + } - if UseSSL { - // HTTPS - if err := server.ListenAndServeTLS(CertificateFile, CertificateKey); err != nil { - Backend.LogError("startWebAPI", "Error listening on '%s': %v\n", WebListen, err) - } - } else { - // HTTP - if err := server.ListenAndServe(); err != nil { - Backend.LogError("startWebAPI", "Error listening on '%s': %v\n", WebListen, err) - } - } + if UseSSL { + // HTTPS + if err := server.ListenAndServeTLS(CertificateFile, CertificateKey); err != nil { + Backend.LogError("startWebAPI", "Error listening on '%s': %v\n", WebListen, err) + } + } else { + // HTTP + if err := server.ListenAndServe(); err != nil { + Backend.LogError("startWebAPI", "Error listening on '%s': %v\n", WebListen, err) + } + } } // EncodeJSON encodes the data as JSON func EncodeJSON(Backend *core.Backend, w http.ResponseWriter, r *http.Request, data interface{}) (err error) { - w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(data) - if err != nil { - Backend.LogError("EncodeJSON", "Error writing data for route '%s': %v\n", r.URL.Path, err) - } + err = json.NewEncoder(w).Encode(data) + if err != nil { + Backend.LogError("EncodeJSON", "Error writing data for route '%s': %v\n", r.URL.Path, err) + } - return err + return err } // DecodeJSON decodes input JSON data server side sent either via GET or POST. It does not limit the maximum amount to read. // In case of error it will automatically send an error to the client. func DecodeJSON(w http.ResponseWriter, r *http.Request, data interface{}) (err error) { - if r.Body == nil { - http.Error(w, "", http.StatusBadRequest) - return errors.New("no data") - } + if r.Body == nil { + http.Error(w, "", http.StatusBadRequest) + return errors.New("no data") + } - err = json.NewDecoder(r.Body).Decode(data) - if err != nil { - http.Error(w, "", http.StatusBadRequest) - return err - } + err = json.NewDecoder(r.Body).Decode(data) + if err != nil { + http.Error(w, "", http.StatusBadRequest) + return err + } - return nil + return nil } // authenticateMiddleware returns a middleware function to be used with mux.Router.Use(). It handles all authentication functionality. func (api *WebapiInstance) authenticateMiddleware(APIKey uuid.UUID) func(http.Handler) http.Handler { - return (func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - keyID, err := uuid.Parse(r.Header.Get("x-api-key")) - if err != nil { // special case for some paths - for _, exceptPath := range api.AllowKeyInParam { - if exceptPath == r.URL.Path { - r.ParseForm() - keyID, err = uuid.Parse(r.Form.Get("k")) - break - } - } - } - if err != nil { // Invalid key format - w.WriteHeader(http.StatusUnauthorized) - return - } + return (func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + keyID, err := uuid.Parse(r.Header.Get("x-api-key")) + if err != nil { // special case for some paths + for _, exceptPath := range api.AllowKeyInParam { + if exceptPath == r.URL.Path { + r.ParseForm() + keyID, err = uuid.Parse(r.Form.Get("k")) + break + } + } + } + if err != nil { // Invalid key format + w.WriteHeader(http.StatusUnauthorized) + return + } - if keyID != APIKey { - w.WriteHeader(http.StatusUnauthorized) - return - } + if keyID != APIKey { + w.WriteHeader(http.StatusUnauthorized) + return + } - next.ServeHTTP(w, r) - }) - }) + next.ServeHTTP(w, r) + }) + }) } diff --git a/webapi/Download_test.go b/webapi/Download_test.go new file mode 100644 index 0000000..8437585 --- /dev/null +++ b/webapi/Download_test.go @@ -0,0 +1,13 @@ +package webapi + +import ( + "fmt" + "testing" +) + +// Test function +func TestDecodeBlake3Hash(t *testing.T) { + hash, bool := DecodeBlake3Hash("") + fmt.Println(hash) + fmt.Println(bool) +} diff --git a/webapi/Search.go b/webapi/Search.go index be4ae29..43b2fed 100644 --- a/webapi/Search.go +++ b/webapi/Search.go @@ -16,61 +16,61 @@ Author: Peter Kleissner package webapi import ( - "net/http" - "strconv" - "time" + "net/http" + "strconv" + "time" - "github.com/google/uuid" + "github.com/google/uuid" ) // SearchRequest is the information from the end-user for the search. Filters and sort order may be applied when starting the search, or at runtime when getting the results. type SearchRequest struct { - Term string `json:"term"` // Search term. - Timeout int `json:"timeout"` // Timeout in seconds. 0 means default. This is the entire time the search may take. Found results are still available after this timeout. - MaxResults int `json:"maxresults"` // Total number of max results. 0 means default. - DateFrom string `json:"datefrom"` // Date from, both from/to are required if set. Format "2006-01-02 15:04:05". - DateTo string `json:"dateto"` // Date to, both from/to are required if set. Format "2006-01-02 15:04:05". - Sort int `json:"sort"` // See SortX. - TerminateID []uuid.UUID `json:"terminate"` // Optional: Previous search IDs to terminate. This is if the user makes a new search from the same tab. Same as first calling /search/terminate. - FileType int `json:"filetype"` // File type such as binary, text document etc. See core.TypeX. -1 = not used. - FileFormat int `json:"fileformat"` // File format such as PDF, Word, Ebook, etc. See core.FormatX. -1 = not used. - SizeMin int `json:"sizemin"` // Min file size in bytes. -1 = not used. - SizeMax int `json:"sizemax"` // Max file size in bytes. -1 = not used. + Term string `json:"term"` // Search term. + Timeout int `json:"timeout"` // Timeout in seconds. 0 means default. This is the entire time the search may take. Found results are still available after this timeout. + MaxResults int `json:"maxresults"` // Total number of max results. 0 means default. + DateFrom string `json:"datefrom"` // Date from, both from/to are required if set. Format "2006-01-02 15:04:05". + DateTo string `json:"dateto"` // Date to, both from/to are required if set. Format "2006-01-02 15:04:05". + Sort int `json:"sort"` // See SortX. + TerminateID []uuid.UUID `json:"terminate"` // Optional: Previous search IDs to terminate. This is if the user makes a new search from the same tab. Same as first calling /search/terminate. + FileType int `json:"filetype"` // File type such as binary, text document etc. See core.TypeX. -1 = not used. + FileFormat int `json:"fileformat"` // File format such as PDF, Word, Ebook, etc. See core.FormatX. -1 = not used. + SizeMin int `json:"sizemin"` // Min file size in bytes. -1 = not used. + SizeMax int `json:"sizemax"` // Max file size in bytes. -1 = not used. } // Sort orders const ( - SortNone = 0 // No sorting. Results are returned as they come in. - SortRelevanceAsc = 1 // Least relevant results first. - SortRelevanceDec = 2 // Most relevant results first. - SortDateAsc = 3 // Oldest first. - SortDateDesc = 4 // Newest first. - SortNameAsc = 5 // File name ascending. The folder name is not used for sorting. - SortNameDesc = 6 // File name descending. The folder name is not used for sorting. - SortSizeAsc = 7 // File size ascending. Smallest files first. - SortSizeDesc = 8 // File size descending. Largest files first. - SortSharedByCountAsc = 9 // Shared by count ascending. Files that are shared by the least count of peers first. - SortSharedByCountDesc = 10 // Shared by count descending. Files that are shared by the most count of peers first. + SortNone = 0 // No sorting. Results are returned as they come in. + SortRelevanceAsc = 1 // Least relevant results first. + SortRelevanceDec = 2 // Most relevant results first. + SortDateAsc = 3 // Oldest first. + SortDateDesc = 4 // Newest first. + SortNameAsc = 5 // File name ascending. The folder name is not used for sorting. + SortNameDesc = 6 // File name descending. The folder name is not used for sorting. + SortSizeAsc = 7 // File size ascending. Smallest files first. + SortSizeDesc = 8 // File size descending. Largest files first. + SortSharedByCountAsc = 9 // Shared by count ascending. Files that are shared by the least count of peers first. + SortSharedByCountDesc = 10 // Shared by count descending. Files that are shared by the most count of peers first. ) // SearchRequestResponse is the result to the initial search request type SearchRequestResponse struct { - ID uuid.UUID `json:"id"` // ID of the search job. This is used to get the results. - Status int `json:"status"` // Status of the search: 0 = Success (ID valid), 1 = Invalid Term, 2 = Error Max Concurrent Searches + ID uuid.UUID `json:"id"` // ID of the search job. This is used to get the results. + Status int `json:"status"` // Status of the search: 0 = Success (ID valid), 1 = Invalid Term, 2 = Error Max Concurrent Searches } // SearchResult contains the search results. type SearchResult struct { - Status int `json:"status"` // Status: 0 = Success with results, 1 = No more results available, 2 = Search ID not found, 3 = No results yet available keep trying - Files []apiFile `json:"files"` // List of files found - Statistic interface{} `json:"statistic"` // Statistics of all results (independent from applied filters), if requested. Only set if files are returned (= if statistics changed). See SearchStatisticData. + Status int `json:"status"` // Status: 0 = Success with results, 1 = No more results available, 2 = Search ID not found, 3 = No results yet available keep trying + Files []apiFile `json:"files"` // List of files found + Statistic interface{} `json:"statistic"` // Statistics of all results (independent from applied filters), if requested. Only set if files are returned (= if statistics changed). See SearchStatisticData. } // SearchStatistic contains statistics on search results. Statistics are always calculated over all results, regardless of any applied runtime filters. type SearchStatistic struct { - SearchStatisticData - Status int `json:"status"` // Status: 0 = Success - IsTerminated bool `json:"terminated"` // Whether the search is terminated, meaning that statistics won't change + SearchStatisticData + Status int `json:"status"` // Status: 0 = Success + IsTerminated bool `json:"terminated"` // Whether the search is terminated, meaning that statistics won't change } const apiDateFormat = "2006-01-02 15:04:05" @@ -84,30 +84,29 @@ Result: 200 on success with JSON SearchRequestResponse 400 on invalid JSON */ func (api *WebapiInstance) apiSearch(w http.ResponseWriter, r *http.Request) { + var input SearchRequest + if err := DecodeJSON(w, r, &input); err != nil { + return + } - var input SearchRequest - if err := DecodeJSON(w, r, &input); err != nil { - return - } + if input.Timeout <= 0 { + input.Timeout = 20 + } + if input.MaxResults <= 0 { + input.MaxResults = 200 + } - if input.Timeout <= 0 { - input.Timeout = 20 - } - if input.MaxResults <= 0 { - input.MaxResults = 200 - } + // Terminate previous searches, if their IDs were supplied. This allows terminating the old search immediately without making a separate /search/terminate request. + for _, terminate := range input.TerminateID { + if job := api.JobLookup(terminate); job != nil { + job.Terminate() + api.RemoveJob(job) + } + } - // Terminate previous searches, if their IDs were supplied. This allows terminating the old search immediately without making a separate /search/terminate request. - for _, terminate := range input.TerminateID { - if job := api.JobLookup(terminate); job != nil { - job.Terminate() - api.RemoveJob(job) - } - } + job := api.dispatchSearch(input) - job := api.dispatchSearch(input) - - EncodeJSON(api.Backend, w, r, SearchRequestResponse{Status: 0, ID: job.id}) + EncodeJSON(api.backend, w, r, SearchRequestResponse{Status: 0, ID: job.id}) } /* @@ -129,82 +128,82 @@ Optional parameters: Result: 200 with JSON structure SearchResult. Check the field status. */ func (api *WebapiInstance) apiSearchResult(w http.ResponseWriter, r *http.Request) { - r.ParseForm() - jobID, err := uuid.Parse(r.Form.Get("id")) - if err != nil { - http.Error(w, "", http.StatusBadRequest) - return - } - limit, err := strconv.Atoi(r.Form.Get("limit")) - if err != nil { - limit = 100 - } - offset, errOffset := strconv.Atoi(r.Form.Get("offset")) + r.ParseForm() + jobID, err := uuid.Parse(r.Form.Get("id")) + if err != nil { + http.Error(w, "", http.StatusBadRequest) + return + } + limit, err := strconv.Atoi(r.Form.Get("limit")) + if err != nil { + limit = 100 + } + offset, errOffset := strconv.Atoi(r.Form.Get("offset")) - // find the job ID - job := api.JobLookup(jobID) - if job == nil { - EncodeJSON(api.Backend, w, r, SearchResult{Status: 2}) - return - } + // find the job ID + job := api.JobLookup(jobID) + if job == nil { + EncodeJSON(api.backend, w, r, SearchResult{Status: 2}) + return + } - // filters and sort parameter - if filterReset, _ := strconv.ParseBool(r.Form.Get("reset")); filterReset { - fileType, _ := strconv.Atoi(r.Form.Get("filetype")) - fileFormat, _ := strconv.Atoi(r.Form.Get("fileformat")) - dateFrom := r.Form.Get("from") - dateTo := r.Form.Get("to") - sort, _ := strconv.Atoi(r.Form.Get("sort")) - sizeMin, _ := strconv.Atoi(r.Form.Get("sizemin")) - sizeMax, _ := strconv.Atoi(r.Form.Get("sizemax")) + // filters and sort parameter + if filterReset, _ := strconv.ParseBool(r.Form.Get("reset")); filterReset { + fileType, _ := strconv.Atoi(r.Form.Get("filetype")) + fileFormat, _ := strconv.Atoi(r.Form.Get("fileformat")) + dateFrom := r.Form.Get("from") + dateTo := r.Form.Get("to") + sort, _ := strconv.Atoi(r.Form.Get("sort")) + sizeMin, _ := strconv.Atoi(r.Form.Get("sizemin")) + sizeMax, _ := strconv.Atoi(r.Form.Get("sizemax")) - filter := inputToSearchFilter(sort, fileType, fileFormat, dateFrom, dateTo, sizeMin, sizeMax) + filter := inputToSearchFilter(sort, fileType, fileFormat, dateFrom, dateTo, sizeMin, sizeMax) - job.RuntimeFilter(filter) - } + job.RuntimeFilter(filter) + } - // query all results - var resultFiles []*apiFile - if errOffset == nil { - resultFiles = job.ReturnResult(offset, limit) - } else { - resultFiles = job.ReturnNext(limit) - } + // query all results + var resultFiles []*apiFile + if errOffset == nil { + resultFiles = job.ReturnResult(offset, limit) + } else { + resultFiles = job.ReturnNext(limit) + } - var result SearchResult - result.Files = []apiFile{} + var result SearchResult + result.Files = []apiFile{} - // loop over results - for n := range resultFiles { - result.Files = append(result.Files, *resultFiles[n]) - } + // loop over results + for n := range resultFiles { + result.Files = append(result.Files, *resultFiles[n]) + } - // set the status - if len(result.Files) > 0 { - if job.IsSearchResults() { - result.Status = 0 // 0 = Success with results - } else { - result.Status = 1 // No more results to expect - } - } else { - switch job.Status { - case SearchStatusLive: - result.Status = 3 // No results yet available keep trying + // set the status + if len(result.Files) > 0 { + if job.IsSearchResults() { + result.Status = 0 // 0 = Success with results + } else { + result.Status = 1 // No more results to expect + } + } else { + switch job.Status { + case SearchStatusLive: + result.Status = 3 // No results yet available keep trying - case SearchStatusTerminated: - result.Status = 1 // No more results to expect + case SearchStatusTerminated: + result.Status = 1 // No more results to expect - default: // SearchStatusNoIndex, SearchStatusNotStarted - result.Status = 1 // No more results to expect - } - } + default: // SearchStatusNoIndex, SearchStatusNotStarted + result.Status = 1 // No more results to expect + } + } - // embedded statistics? - if returnStats, _ := strconv.ParseBool(r.Form.Get("stats")); returnStats { - result.Statistic = job.Statistics() - } + // embedded statistics? + if returnStats, _ := strconv.ParseBool(r.Form.Get("stats")); returnStats { + result.Statistic = job.Statistics() + } - EncodeJSON(api.Backend, w, r, result) + EncodeJSON(api.backend, w, r, result) } /* @@ -216,80 +215,80 @@ Result: If successful, upgrades to a websocket and sends JSON structure Sear Limit is optional. Not used if ommitted or 0. */ func (api *WebapiInstance) apiSearchResultStream(w http.ResponseWriter, r *http.Request) { - r.ParseForm() - jobID, err := uuid.Parse(r.Form.Get("id")) - if err != nil { - http.Error(w, "", http.StatusBadRequest) - return - } - limit, err := strconv.Atoi(r.Form.Get("limit")) - useLimit := err == nil + r.ParseForm() + jobID, err := uuid.Parse(r.Form.Get("id")) + if err != nil { + http.Error(w, "", http.StatusBadRequest) + return + } + limit, err := strconv.Atoi(r.Form.Get("limit")) + useLimit := err == nil - // look up the job - job := api.JobLookup(jobID) - if job == nil { - EncodeJSON(api.Backend, w, r, SearchResult{Status: 2}) - return - } + // look up the job + job := api.JobLookup(jobID) + if job == nil { + EncodeJSON(api.backend, w, r, SearchResult{Status: 2}) + return + } - // upgrade to websocket - conn, err := WSUpgrader.Upgrade(w, r, nil) - if err != nil { - // gorilla will automatically respond with "400 Bad Request", no other response is therefore necessary - return - } + // upgrade to websocket + conn, err := WSUpgrader.Upgrade(w, r, nil) + if err != nil { + // gorilla will automatically respond with "400 Bad Request", no other response is therefore necessary + return + } - defer conn.Close() + defer conn.Close() - // loop to get new results and send out via the web socket. - // Only exit if limit is reached if used, otherwise only if there are no result or the connection breaks. - for { - // query all results - var resultFiles []*apiFile + // loop to get new results and send out via the web socket. + // Only exit if limit is reached if used, otherwise only if there are no result or the connection breaks. + for { + // query all results + var resultFiles []*apiFile - queryCount := 1 - if useLimit { - queryCount = limit - } - resultFiles = job.ReturnNext(queryCount) + queryCount := 1 + if useLimit { + queryCount = limit + } + resultFiles = job.ReturnNext(queryCount) - if useLimit { - limit -= len(resultFiles) - } + if useLimit { + limit -= len(resultFiles) + } - // loop over results - var result SearchResult - result.Files = []apiFile{} + // loop over results + var result SearchResult + result.Files = []apiFile{} - for n := range resultFiles { - result.Files = append(result.Files, *resultFiles[n]) - } + for n := range resultFiles { + result.Files = append(result.Files, *resultFiles[n]) + } - if !job.IsSearchResults() { - result.Status = 1 // No more results to expect + if !job.IsSearchResults() { + result.Status = 1 // No more results to expect - if len(result.Files) == 0 { - conn.WriteJSON(result) // final message - return - } - } + if len(result.Files) == 0 { + conn.WriteJSON(result) // final message + return + } + } - // if no results, stall - if len(result.Files) == 0 { - time.Sleep(time.Millisecond * 100) - continue - } + // if no results, stall + if len(result.Files) == 0 { + time.Sleep(time.Millisecond * 100) + continue + } - // send out the results via the websocket - if err := conn.WriteJSON(result); err != nil { - return - } + // send out the results via the websocket + if err := conn.WriteJSON(result); err != nil { + return + } - // Check whether to continue. If the limit is used break once all done. - if (useLimit && limit <= 0) || result.Status == 1 { - break - } - } + // Check whether to continue. If the limit is used break once all done. + if (useLimit && limit <= 0) || result.Status == 1 { + break + } + } } /* @@ -303,25 +302,25 @@ Response: 204 Empty */ func (api *WebapiInstance) apiSearchTerminate(w http.ResponseWriter, r *http.Request) { - r.ParseForm() - jobID, err := uuid.Parse(r.Form.Get("id")) - if err != nil { - http.Error(w, "", http.StatusBadRequest) - return - } + r.ParseForm() + jobID, err := uuid.Parse(r.Form.Get("id")) + if err != nil { + http.Error(w, "", http.StatusBadRequest) + return + } - // look up the job - job := api.JobLookup(jobID) - if job == nil { - http.Error(w, "", http.StatusNotFound) - return - } + // look up the job + job := api.JobLookup(jobID) + if job == nil { + http.Error(w, "", http.StatusNotFound) + return + } - // terminate and remove it from the list - job.Terminate() - api.RemoveJob(job) + // terminate and remove it from the list + job.Terminate() + api.RemoveJob(job) - w.WriteHeader(http.StatusNoContent) + w.WriteHeader(http.StatusNoContent) } /* @@ -331,23 +330,23 @@ Request: GET /search/result?id=[UUID] Result: 200 with JSON structure SearchStatistic. Check the field status (0 = Success, 2 = ID not found). */ func (api *WebapiInstance) apiSearchStatistic(w http.ResponseWriter, r *http.Request) { - r.ParseForm() - jobID, err := uuid.Parse(r.Form.Get("id")) - if err != nil { - http.Error(w, "", http.StatusBadRequest) - return - } + r.ParseForm() + jobID, err := uuid.Parse(r.Form.Get("id")) + if err != nil { + http.Error(w, "", http.StatusBadRequest) + return + } - // find the job ID - job := api.JobLookup(jobID) - if job == nil { - EncodeJSON(api.Backend, w, r, SearchStatistic{Status: 2}) - return - } + // find the job ID + job := api.JobLookup(jobID) + if job == nil { + EncodeJSON(api.backend, w, r, SearchStatistic{Status: 2}) + return + } - stats := job.Statistics() + stats := job.Statistics() - EncodeJSON(api.Backend, w, r, SearchStatistic{SearchStatisticData: stats, Status: 0, IsTerminated: job.IsTerminated()}) + EncodeJSON(api.backend, w, r, SearchStatistic{SearchStatisticData: stats, Status: 0, IsTerminated: job.IsTerminated()}) } /* @@ -358,65 +357,100 @@ Request: GET /explore?limit=[max records]&type=[file type]&offset=[offset] Result: 200 with JSON structure SearchResult. Check the field status. */ func (api *WebapiInstance) apiExplore(w http.ResponseWriter, r *http.Request) { - r.ParseForm() - offset, _ := strconv.Atoi(r.Form.Get("offset")) - limit, err := strconv.Atoi(r.Form.Get("limit")) - if err != nil { - limit = 100 - } - fileType, err := strconv.Atoi(r.Form.Get("type")) - if err != nil { - fileType = -1 - } + r.ParseForm() + offset, _ := strconv.Atoi(r.Form.Get("offset")) + limit, err := strconv.Atoi(r.Form.Get("limit")) + if err != nil { + limit = 100 + } - resultFiles := api.queryRecentShared(api.Backend, fileType, uint64(limit*20/100), uint64(offset), uint64(limit)) + fileType, err := strconv.Atoi(r.Form.Get("type")) + if err != nil { + fileType = -1 + } - var result SearchResult - result.Files = []apiFile{} + result := api.ExploreHelper(fileType,limit,offset,[]byte{},false) - // loop over results - for n := range resultFiles { - result.Files = append(result.Files, blockRecordFileToAPI(resultFiles[n])) - } + EncodeJSON(api.backend, w, r, result) +} - if len(result.Files) == 0 { - result.Status = 3 // No results yet available keep trying - } +/* +apiExploreNodeID returns the shared files of a particular node in Peernet. Results are returned in real-time. The file type is an optional filter. See TypeX. +Special type -2 = Binary, Compressed, Container, Executable. This special type includes everything except Documents, Video, Audio, Ebooks, Picture, Text. - result.Status = 1 // No more results to expect +Request: GET /explore/node?limit=[max records]&type=[file type]&offset=[offset]&NodeID=[node id] +Result: 200 with JSON structure SearchResult. Check the field status. +*/ +func (api *WebapiInstance) apiExploreNodeID(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + offset, _ := strconv.Atoi(r.Form.Get("offset")) + limit, err := strconv.Atoi(r.Form.Get("limit")) + if err != nil { + limit = 100 + } + // ID fields for results for a specific node ID. + NodeId, _ := DecodeBlake3Hash(r.Form.Get("NodeID")) - EncodeJSON(api.Backend, w, r, result) + fileType, err := strconv.Atoi(r.Form.Get("type")) + if err != nil { + fileType = -1 + } + + result := api.ExploreHelper(fileType,limit,offset,NodeId,true) + + EncodeJSON(api.backend, w, r, result) +} + +// ExploreHelper Helper function for the explore route with the possibility search based on a node ID +func (api *WebapiInstance) ExploreHelper(fileType int, limit, offset int, nodeID []byte, nodeIDState bool) *SearchResult{ + resultFiles := api.queryRecentShared(api.backend, fileType, uint64(limit*20/100), uint64(offset), uint64(limit), nodeID, nodeIDState) + + var result SearchResult + result.Files = []apiFile{} + + // loop over results + for n := range resultFiles { + result.Files = append(result.Files, blockRecordFileToAPI(resultFiles[n])) + } + + if len(result.Files) == 0 { + result.Status = 3 // No results yet available keep trying + } + + result.Status = 1 // No more results to expect + + return &result } func (input *SearchRequest) Parse() (Timeout time.Duration) { - if input.Timeout == 0 { - Timeout = time.Second * 20 // default timeout: 20 seconds - } else { - Timeout = time.Duration(input.Timeout) * time.Second - } + if input.Timeout == 0 { + Timeout = time.Second * 20 // default timeout: 20 seconds + } else { + Timeout = time.Duration(input.Timeout) * time.Second + } - return + return } // ToSearchFilter converts the user input to a valid search filter func (input *SearchRequest) ToSearchFilter() (output SearchFilter) { - return inputToSearchFilter(input.Sort, input.FileType, input.FileFormat, input.DateFrom, input.DateTo, input.SizeMin, input.SizeMax) + return inputToSearchFilter(input.Sort, input.FileType, input.FileFormat, input.DateFrom, input.DateTo, input.SizeMin, input.SizeMax) } func inputToSearchFilter(Sort, FileType, FileFormat int, DateFrom, DateTo string, SizeMin, SizeMax int) (output SearchFilter) { - output.Sort = Sort - output.FileType = FileType - output.FileFormat = FileFormat - output.SizeMin = SizeMin - output.SizeMax = SizeMax + output.Sort = Sort + output.FileType = FileType + output.FileFormat = FileFormat + output.SizeMin = SizeMin + output.SizeMax = SizeMax - dateFrom, errFrom := time.Parse(apiDateFormat, DateFrom) - dateTo, errTo := time.Parse(apiDateFormat, DateTo) - if errFrom == nil && errTo == nil && dateFrom.Before(dateTo) { - output.DateFrom = dateFrom - output.DateTo = dateTo - output.IsDates = true - } + dateFrom, errFrom := time.Parse(apiDateFormat, DateFrom) + dateTo, errTo := time.Parse(apiDateFormat, DateTo) + if errFrom == nil && errTo == nil && dateFrom.Before(dateTo) { + output.DateFrom = dateFrom + output.DateTo = dateTo + output.IsDates = true + } - return + return } diff --git a/webapi/Shared Recent.go b/webapi/Shared Recent.go index af91572..ed44227 100644 --- a/webapi/Shared Recent.go +++ b/webapi/Shared Recent.go @@ -6,84 +6,98 @@ Author: Peter Kleissner package webapi import ( - "fmt" + "fmt" + "time" - "github.com/PeernetOfficial/core" - "github.com/PeernetOfficial/core/blockchain" + "github.com/PeernetOfficial/core" + "github.com/PeernetOfficial/core/blockchain" ) // queryRecentShared returns recently shared files on the network from random peers until the limit is reached. -func (api *WebapiInstance) queryRecentShared(backend *core.Backend, fileType int, limitPeer, offsetTotal, limitTotal uint64) (files []blockchain.BlockRecordFile) { - if limitPeer == 0 { - limitPeer = 1 - } +func (api *WebapiInstance) queryRecentShared(backend *core.Backend, fileType int, limitPeer, offsetTotal, limitTotal uint64, nodeID []byte, nodeIDState bool) (files []blockchain.BlockRecordFile) { + if limitPeer == 0 { + limitPeer = 1 + } - // Use the peer list to know about active peers. Random order! - peerList := api.Backend.PeerlistGet() + // Assign peer list as an empty array + var peerList []*core.PeerInfo - // Files from peers exceeding the limit. It is used if from all peers the total limit is not reached. - var filesSeconday []blockchain.BlockRecordFile + // check if the NodeID is provided or not + if len(nodeID) == 0 && !nodeIDState { + // Use the peer list to know about active peers. Random order! + peerList = api.backend.PeerlistGet() + } else { + // Get peer information based on the NodeID provided + _, peer, err := api.backend.FindNode(nodeID, time.Second*5) + if err != nil { + return + } + peerList = append(peerList, peer) + } - for _, peer := range peerList { - if peer.BlockchainHeight == 0 { - continue - } + // Files from peers exceeding the limit. It is used if from all peers the total limit is not reached. + var filesSeconday []blockchain.BlockRecordFile - var filesFromPeer uint64 + for _, peer := range peerList { + if peer.BlockchainHeight == 0 { + continue + } - // decode blocks from top down - blockLoop: - for blockN := peer.BlockchainHeight - 1; blockN > 0; blockN-- { - blockDecoded, _, found, _ := backend.ReadBlock(peer.PublicKey, peer.BlockchainVersion, blockN) - if !found { - continue - } + var filesFromPeer uint64 - for _, record := range blockDecoded.RecordsDecoded { - if file, ok := record.(blockchain.BlockRecordFile); ok && isFileTypeMatchBlock(&file, fileType) { - // add the tags 'Shared By Count' and 'Shared By GeoIP' - file.Tags = append(file.Tags, blockchain.TagFromNumber(blockchain.TagSharedByCount, 1)) - if latitude, longitude, valid := api.Peer2GeoIP(peer); valid { - sharedByGeoIP := fmt.Sprintf("%.4f", latitude) + "," + fmt.Sprintf("%.4f", longitude) - file.Tags = append(file.Tags, blockchain.TagFromText(blockchain.TagSharedByGeoIP, sharedByGeoIP)) - } + // decode blocks from top down + blockLoop: + for blockN := peer.BlockchainHeight - 1; blockN > 0; blockN-- { + blockDecoded, _, found, _ := backend.ReadBlock(peer.PublicKey, peer.BlockchainVersion, blockN) + if !found { + continue + } - // found a new file! append. - if filesFromPeer < limitPeer { - filesFromPeer++ + for _, record := range blockDecoded.RecordsDecoded { + if file, ok := record.(blockchain.BlockRecordFile); ok && isFileTypeMatchBlock(&file, fileType) { + // add the tags 'Shared By Count' and 'Shared By GeoIP' + file.Tags = append(file.Tags, blockchain.TagFromNumber(blockchain.TagSharedByCount, 1)) + if latitude, longitude, valid := api.Peer2GeoIP(peer); valid { + sharedByGeoIP := fmt.Sprintf("%.4f", latitude) + "," + fmt.Sprintf("%.4f", longitude) + file.Tags = append(file.Tags, blockchain.TagFromText(blockchain.TagSharedByGeoIP, sharedByGeoIP)) + } - if offsetTotal > 0 { - offsetTotal-- - continue - } + // found a new file! append. + if filesFromPeer < limitPeer { + filesFromPeer++ - files = append(files, file) + if offsetTotal > 0 { + offsetTotal-- + continue + } - if uint64(len(files)) >= limitTotal { - return - } - } else if uint64(len(filesSeconday)) < limitTotal-uint64(len(files)) { - filesSeconday = append(filesSeconday, file) - } else { - break blockLoop - } - } - } - } - } + files = append(files, file) - files = append(files, filesSeconday...) + if uint64(len(files)) >= limitTotal { + return + } + } else if uint64(len(filesSeconday)) < limitTotal-uint64(len(files)) { + filesSeconday = append(filesSeconday, file) + } else { + break blockLoop + } + } + } + } + } - return + files = append(files, filesSeconday...) + + return } // isFileTypeMatchBlock checks if the file type matches. -1 = accept any. -2 = core.TypeBinary, core.TypeCompressed, core.TypeContainer, core.TypeExecutable. func isFileTypeMatchBlock(file *blockchain.BlockRecordFile, fileType int) bool { - if fileType == -1 { - return true - } else if fileType == -2 { - return file.Type == core.TypeBinary || file.Type == core.TypeCompressed || file.Type == core.TypeContainer || file.Type == core.TypeExecutable - } + if fileType == -1 { + return true + } else if fileType == -2 { + return file.Type == core.TypeBinary || file.Type == core.TypeCompressed || file.Type == core.TypeContainer || file.Type == core.TypeExecutable + } - return file.Type == uint8(fileType) + return file.Type == uint8(fileType) }