From e88f73fad10ceb5adc8c69f52e0c2cb0a06d8752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Hagberg?= Date: Thu, 8 Feb 2018 15:48:47 +0100 Subject: [PATCH 01/16] Basic frontend with mock API call results --- .gitignore | 1 + server/website/background.png | Bin 0 -> 7572 bytes server/website/browse.html | 33 ++++++++ server/website/index.html | 64 ++++++++++++++++ server/website/js/browse.js | 10 +++ server/website/js/functions.js | 71 ++++++++++++++++++ server/website/js/index.js | 11 +++ server/website/js/search.js | 16 ++++ server/website/mockapi/awaiting_approval.json | 14 ++++ server/website/mockapi/browsefile.json | 8 ++ server/website/mockapi/browsehost.json | 31 ++++++++ server/website/mockapi/latestnewmachines.json | 9 +++ server/website/mockapi/search.json | 33 ++++++++ server/website/mockapi/systemstatus_data.json | 5 ++ server/website/search.html | 55 ++++++++++++++ server/website/style.css | 37 +++++++++ .../templates/awaiting_approval.handlebars | 44 +++++++++++ .../website/templates/browsefile.handlebars | 26 +++++++ .../website/templates/browsehost.handlebars | 36 +++++++++ .../templates/latestnewmachines.handlebars | 3 + server/website/templates/search.handlebars | 14 ++++ .../website/templates/systemstatus.handlebars | 10 +++ 22 files changed, 531 insertions(+) create mode 100644 server/website/background.png create mode 100644 server/website/browse.html create mode 100644 server/website/index.html create mode 100644 server/website/js/browse.js create mode 100644 server/website/js/functions.js create mode 100644 server/website/js/index.js create mode 100644 server/website/js/search.js create mode 100644 server/website/mockapi/awaiting_approval.json create mode 100644 server/website/mockapi/browsefile.json create mode 100644 server/website/mockapi/browsehost.json create mode 100644 server/website/mockapi/latestnewmachines.json create mode 100644 server/website/mockapi/search.json create mode 100644 server/website/mockapi/systemstatus_data.json create mode 100644 server/website/search.html create mode 100644 server/website/style.css create mode 100644 server/website/templates/awaiting_approval.handlebars create mode 100644 server/website/templates/browsefile.handlebars create mode 100644 server/website/templates/browsehost.handlebars create mode 100644 server/website/templates/latestnewmachines.handlebars create mode 100644 server/website/templates/search.handlebars create mode 100644 server/website/templates/systemstatus.handlebars diff --git a/.gitignore b/.gitignore index 2a3754f..4ef0994 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.exe server/web/web server/jobrunner/jobrunner +server/website/libs diff --git a/server/website/background.png b/server/website/background.png new file mode 100644 index 0000000000000000000000000000000000000000..84427a0c1d0dc88693b12af27917ccfb74ee6453 GIT binary patch literal 7572 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 literal 0 HcmV?d00001 diff --git a/server/website/browse.html b/server/website/browse.html new file mode 100644 index 0000000..405d2d5 --- /dev/null +++ b/server/website/browse.html @@ -0,0 +1,33 @@ + + + + + + Hello UiO! + + + + + + + + + + + + +
+
+
+
+ + diff --git a/server/website/index.html b/server/website/index.html new file mode 100644 index 0000000..870c900 --- /dev/null +++ b/server/website/index.html @@ -0,0 +1,64 @@ + + + + + + Hello UiO! + + + + + + + + + + + + +
+
+

System status

+

+
+
+ + + +
+
+
+
+

Machines

+

+
+
+
+ + + + +
+
+ +
+
+
+
+
+
+
+
+ + diff --git a/server/website/js/browse.js b/server/website/js/browse.js new file mode 100644 index 0000000..057e291 --- /dev/null +++ b/server/website/js/browse.js @@ -0,0 +1,10 @@ +$(document).ready(function(){ + var p = getUrlParams(); + if (p['c']) { + APIcall("mockapi/browsehost.json", "browsehost", + "#placeholder_browse"); + } else if (p['fid']) { + APIcall("mockapi/browsefile.json", "browsefile", + "#placeholder_browse"); + } +}); diff --git a/server/website/js/functions.js b/server/website/js/functions.js new file mode 100644 index 0000000..7e79877 --- /dev/null +++ b/server/website/js/functions.js @@ -0,0 +1,71 @@ +// http://berzniz.com/post/24743062344/handling-handlebarsjs-like-a-pro +function renderTemplate(name, templateValues, callback) { + if (Handlebars.templates === undefined || Handlebars.templates[name] === undefined) { + // must load and compile the template first + $.ajax({ + url : 'templates/' + name + '.handlebars', + success : function(data) { + // compile and keep + if (Handlebars.templates === undefined) { + Handlebars.templates = {}; + } + console.log("Compiling " + name + ".handlebars"); + Handlebars.templates[name] = Handlebars.compile(data, {"strict":true}); + // now, run the template + var output = Handlebars.templates[name](templateValues); + callback(output); + }, + dataType : 'text' + }).fail(function(jqxhr, textStatus, error){ + throw error; + }); + } else { + callback(Handlebars.templates[name](templateValues)); + } +} + +function showError(error, domElement, faIconName) { + $(domElement).html(''); +} + +function APIcall(url, templateName, domElement) { + var deferredObj = $.Deferred(); + $.getJSON(url, function(data){ + try { + renderTemplate(templateName, data, function(output){ + $(domElement).html(output); + deferredObj.resolve(); + }); + } + catch(error) { + showError(error, domElement, "fa-exclamation-triangle"); + deferredObj.resolve(); + } + }) + .fail(function(jqxhr, textStatus, error){ + if (jqxhr.status == 404) + showError(error, domElement, "fa-unlink"); + else + showError(error, domElement, "fa-exclamation-circle"); + deferredObj.resolve(); + }); + return deferredObj; +} + +// Reads the page's URL parameters and returns them as an associative array +function getUrlParams() { + var vars = []; + var start = window.location.href.indexOf('?') + 1; + if (start == 0) { return vars; } + var end = window.location.href.indexOf('#'); + if (end < 0) end = window.location.href.length; + var pairs = window.location.href.slice(start,end).split('&'); + for (var i = 0; i < pairs.length; i++) { + pair = pairs[i].split('='); + vars[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1].replace(/\+/g,' ')); + } + return vars; +} diff --git a/server/website/js/index.js b/server/website/js/index.js new file mode 100644 index 0000000..7b94276 --- /dev/null +++ b/server/website/js/index.js @@ -0,0 +1,11 @@ +$(document).ready(function(){ + p = getUrlParams(); + $('input#search').val(p['q']); + + APIcall("mockapi/systemstatus_data.json", "systemstatus", + $('#placeholder_systemstatus')); + APIcall("mockapi/awaiting_approval.json", "awaiting_approval", + $('#placeholder_approval')); + APIcall("mockapi/latestnewmachines.json", "latestnewmachines", + $('#placeholder_latestnewmachines')); +}); diff --git a/server/website/js/search.js b/server/website/js/search.js new file mode 100644 index 0000000..705b2cb --- /dev/null +++ b/server/website/js/search.js @@ -0,0 +1,16 @@ +var spinnerhtml; + +$(document).ready(function(){ + var p = getUrlParams(); + $('input#search').val(p['q']); + spinnerhtml = $("#placeholder_searchresult").html(); + APIcall("mockapi/search.json", "search", "#placeholder_searchresult"); +}); + +function newsearch() { + // put the spinner back + $('#placeholder_searchresult').html(spinnerhtml); + // perform the new search + var q = $('input#search').val(); + APIcall("mockapi/search.json", "search", "#placeholder_searchresult"); +} diff --git a/server/website/mockapi/awaiting_approval.json b/server/website/mockapi/awaiting_approval.json new file mode 100644 index 0000000..7b8b0e0 --- /dev/null +++ b/server/website/mockapi/awaiting_approval.json @@ -0,0 +1,14 @@ +{ + "awaitingapproval": [ + { + "ipaddress": "127.0.0.1", + "reversedns": "localhost", + "oshostname": "liksom.local" + }, + { + "ipaddress": "129.240.202.63", + "reversedns": "callisto.uio.no", + "oshostname": "callisto.uio.no" + } + ] +} diff --git a/server/website/mockapi/browsefile.json b/server/website/mockapi/browsefile.json new file mode 100644 index 0000000..4148184 --- /dev/null +++ b/server/website/mockapi/browsefile.json @@ -0,0 +1,8 @@ +{ + "filename": "/usr/sbin/dmidecode -t system", + "type": "command", + "lastmodified": "2018-02-08T13:00:22+01", + "content": "Line one\nLine two\nLine three", + "certfp": 817263, + "hostname": "flyndre.example.no" +} diff --git a/server/website/mockapi/browsehost.json b/server/website/mockapi/browsehost.json new file mode 100644 index 0000000..0590f95 --- /dev/null +++ b/server/website/mockapi/browsehost.json @@ -0,0 +1,31 @@ +{ + "details": { + "ipaddress": "123.123.123.123", + "hostname": "flyndre.example.no", + "lastseen": "2018-02-08T11:22:33+01", + "os": "Fedora 27", + "os_edition": "Workstation", + "kernel": "4.14.16-300.fc27.x86_64", + "vendor": "Dell Inc.", + "model": "OptiPlex 7010", + "serial": "1YF2YF3", + "certfp": "298376ACBDFCBAFDC589356864ABCDEF", + "clientversion": "0.1.2" + }, + "files": [ + { + "filename": "/etc/redhat-release", + "fileid": 123 + } + ], + "commands": [ + { + "filename": "/bin/uname -a", + "fileid": 234 + }, + { + "filename": "/usr/sbin/dmidecode -t system", + "fileid": 345 + } + ] +} diff --git a/server/website/mockapi/latestnewmachines.json b/server/website/mockapi/latestnewmachines.json new file mode 100644 index 0000000..d812d1d --- /dev/null +++ b/server/website/mockapi/latestnewmachines.json @@ -0,0 +1,9 @@ +{ + "latestnewmachines": [ + { + "hostname": "flyndre.example.no", + "certfp": "1234ABCD", + "firstseen": "2018-02-04T13:23:34+01" + } + ] +} diff --git a/server/website/mockapi/search.json b/server/website/mockapi/search.json new file mode 100644 index 0000000..9bb5656 --- /dev/null +++ b/server/website/mockapi/search.json @@ -0,0 +1,33 @@ +{ + "query": "27", + "numberOfHits": 3, + "hits": [ + { + "fileid": 123, + "filename": "/etc/redhat-release", + "type": "file", + "excerpt": "Fedora release 27 (Twenty Seven)", + "hostname": "flyndre", + "certfp": "ABCD1234", + "number": 1 + }, + { + "fileid": 456, + "filename": "/bin/uname -a", + "type": "command", + "excerpt": "yndre 4.14.14-300.fc27.x86_64 #1 SMP Fri J", + "hostname": "flyndre", + "certfp": "DEFE3232", + "number": 2 + }, + { + "fileid": 789, + "filename": "/usr/sbin/dmidecode -t system", + "type": "command", + "excerpt": "0x0100, DMI type 1, 27 + + + + + Hello UiO! + + + + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ +
+
+
+
+
+         + + + +
+
+
+ + diff --git a/server/website/style.css b/server/website/style.css new file mode 100644 index 0000000..8e78fab --- /dev/null +++ b/server/website/style.css @@ -0,0 +1,37 @@ +html { + background-image: url('background.png'); + background-repeat: repeat; + background-color: rgba(255,255,255,0.5); +} + +hr { + background-color: #999; +} + +p { + margin-bottom: 0.5em; +} + +th { + padding-right: 1em; +} + +span.excerpt em { + font-weight: normal; + font-style: normal; + background-color: rgba(255,255,200,0.4); +} + +div.filecontent { + white-space: pre; + font-family: monospace; + background-color: rgba(0,0,0,0.1); +} + +.color-approve { + color:#060; +} + +.color-deny { + color:#600; +} diff --git a/server/website/templates/awaiting_approval.handlebars b/server/website/templates/awaiting_approval.handlebars new file mode 100644 index 0000000..e0fd4b1 --- /dev/null +++ b/server/website/templates/awaiting_approval.handlebars @@ -0,0 +1,44 @@ +{{#if awaitingapproval}} +
+

Waiting for approval

+

+ These machines are from ip address ranges not covered + by the ruleset, and must be manually approved/denied. +

+{{#each awaitingapproval}} +
+
+

+ + +

+
+
+ + + + + + + + + +
IP address{{ipaddress}}
Reverse DNS{{reversedns}}
Hostname from OS{{oshostname}}
Name in Nivlheim +
+ + +
+
+
+{{/each}} +{{/if}} diff --git a/server/website/templates/browsefile.handlebars b/server/website/templates/browsefile.handlebars new file mode 100644 index 0000000..2728d5f --- /dev/null +++ b/server/website/templates/browsefile.handlebars @@ -0,0 +1,26 @@ +

{{filename}}

+

From + + {{hostname}}

+ +
+ +
+
+ +
+ + + +
+
+ +
{{content}}
diff --git a/server/website/templates/browsehost.handlebars b/server/website/templates/browsehost.handlebars new file mode 100644 index 0000000..9da6f2c --- /dev/null +++ b/server/website/templates/browsehost.handlebars @@ -0,0 +1,36 @@ +

{{details.hostname}}

+ +

+{{#with details}} + + + + + + + + + + + +
IP address{{ipaddress}}
Last seen{{lastseen}}
OS{{os}}
OS edition{{os_edition}}
Kernel{{kernel}}
Hardware vendor{{vendor}}
Hardware model{{model}}
Serial number{{serial}}
Certificate fingerprint{{certfp}}
Client version{{clientversion}}
+{{/with}} +

+ +

+

Files

+{{#each files}} +{{filename}}
+{{/each}} +

+ +

+

Commands

+{{#each commands}} +{{filename}}
+{{/each}} +

diff --git a/server/website/templates/latestnewmachines.handlebars b/server/website/templates/latestnewmachines.handlebars new file mode 100644 index 0000000..0c4eaaa --- /dev/null +++ b/server/website/templates/latestnewmachines.handlebars @@ -0,0 +1,3 @@ +{{#each latestnewmachines}} +{{hostname}} {{firstseen}} +{{/each}} diff --git a/server/website/templates/search.handlebars b/server/website/templates/search.handlebars new file mode 100644 index 0000000..58568cd --- /dev/null +++ b/server/website/templates/search.handlebars @@ -0,0 +1,14 @@ +

+{{numberOfHits}} hits +{{#if hits}}:{{/if}} +

+ +{{#each hits}} +

+ + {{hostname}}: + + {{filename}}
+ {{{excerpt}}} +

+{{/each}} diff --git a/server/website/templates/systemstatus.handlebars b/server/website/templates/systemstatus.handlebars new file mode 100644 index 0000000..8c629a7 --- /dev/null +++ b/server/website/templates/systemstatus.handlebars @@ -0,0 +1,10 @@ +

+{{filesLastHour}} +files were collected during the last hour.
+{{totalMachines}} +machines are in the system.
+{{#if reportingPercentage}} +{{reportingPercentage}}% +of machines sent in files during the last hour.
+{{/if}} +

From 675cd384ec9fe8d80a5a8a1a0795fc3e62a15ecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Hagberg?= Date: Tue, 13 Feb 2018 16:36:55 +0100 Subject: [PATCH 02/16] Working on API functions --- server/jobrunner/api.go | 81 ++++++++++++++++++ server/jobrunner/api.txt | 20 +++++ server/jobrunner/api_awaitingApproval.go | 70 ++++++++++++++++ server/jobrunner/api_file.go | 84 +++++++++++++++++++ server/jobrunner/taskrunner.go | 15 +++- server/website/js/index.js | 4 +- server/website/mockapi/awaiting_approval.json | 16 ++-- server/website/mockapi/browsefile.json | 6 +- server/website/mockapi/browsehost.json | 14 ++-- .../templates/awaiting_approval.handlebars | 12 +-- .../website/templates/browsefile.handlebars | 2 +- .../website/templates/browsehost.handlebars | 14 ++-- 12 files changed, 305 insertions(+), 33 deletions(-) create mode 100644 server/jobrunner/api.go create mode 100644 server/jobrunner/api.txt create mode 100644 server/jobrunner/api_awaitingApproval.go create mode 100644 server/jobrunner/api_file.go diff --git a/server/jobrunner/api.go b/server/jobrunner/api.go new file mode 100644 index 0000000..d0befc1 --- /dev/null +++ b/server/jobrunner/api.go @@ -0,0 +1,81 @@ +package main + +// Create tasks to parse new files that have been read into the database +import ( + "database/sql" + "encoding/json" + "fmt" + "log" + "net/http" + "strings" + "time" +) + +func runAPI(theDB *sql.DB, port int) { + http.Handle("/api/v0/awaitingApproval", &apiMethodAwaitingApproval{db: theDB}) + http.Handle("/api/v0/file", &apiMethodFile{db: theDB}) + log.Printf("Serving API requests on localhost:%d\n", port) + log.Println(http.ListenAndServe(fmt.Sprintf("localhost:%d", port), nil)) +} + +// returnJSON marshals the given object and writes it as the response, +// and also sets the proper Content-Type header. +// Remember to return after calling this function. +func returnJSON(w http.ResponseWriter, req *http.Request, data interface{}) { + bytes, err := json.MarshalIndent(data, "", " ") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Println(err.Error()) + return + } + bytes = append(bytes, 0xA) // end with a line feed, because I'm a nice person + w.Header().Set("Content-Type", "application/json; charset=utf-8") + // If originating from localhost (typically on another port), + // allow that origin. This makes development easier. + if strings.Index(req.Header.Get("Origin"), "http://127.0.0.1") == 0 { + w.Header().Set("Access-Control-Allow-Origin", "http://127.0.0.1:8000") + } + w.Write(bytes) +} + +type jsonTime time.Time + +func (jst jsonTime) MarshalJSON() ([]byte, error) { + tt := time.Time(jst) + if tt.IsZero() { + return []byte("\"\""), nil + } + return []byte(fmt.Sprintf("\"%s\"", tt.Format(time.RFC3339))), nil +} + +type httpError struct { + message string + code int +} + +func unpackFieldParam(fieldParam string, allowedFields []string) (map[string]bool, *httpError) { + if fieldParam == "" { + return nil, &httpError{ + message: "Missing or empty parameter: fields", + code: http.StatusUnprocessableEntity, + } + } + fields := make(map[string]bool) + for _, f := range strings.Split(fieldParam, ",") { + ok := false + for _, af := range allowedFields { + if strings.EqualFold(f, af) { + ok = true + fields[strings.ToLower(f)] = true + break + } + } + if !ok { + return nil, &httpError{ + message: "Unsupported field name: " + f, + code: http.StatusUnprocessableEntity, + } + } + } + return fields, nil +} diff --git a/server/jobrunner/api.txt b/server/jobrunner/api.txt new file mode 100644 index 0000000..4de86b6 --- /dev/null +++ b/server/jobrunner/api.txt @@ -0,0 +1,20 @@ +Liste over maskiner som venter på manuell godkjenning: +/api/v0/awaitingapproval?fields=ipAddress,reverseDns,osHostname + +Hente 1 fil: +/api/v0/file?fileid=1234&fields=filename,content + +Hente siste versjon av en fil: +/api/v0/file?filename="/usr/sbin/dmidecode -t system"&hostname=callisto.uio.no&fields=content + +Liste ut filer fra en bestemt maskin: +/api/v0/host?hostname=callisto.uio.no&fields=files,commands + +Hente noen detaljer for en bestemt maskin: +/api/v0/host?hostname=callisto.uio.no&fields=details[lastseen,kernel] + +Liste alle maskiner som kjører Linux: +/api/v0/host?os=Linux&fields=details[hostname] + +Søke i filer: +/api/v0/search?q=søkeord&limit=10&offset=0&excerpt=80 diff --git a/server/jobrunner/api_awaitingApproval.go b/server/jobrunner/api_awaitingApproval.go new file mode 100644 index 0000000..e55fdb0 --- /dev/null +++ b/server/jobrunner/api_awaitingApproval.go @@ -0,0 +1,70 @@ +package main + +import ( + "database/sql" + "net" + "net/http" + "strings" + + "github.com/lib/pq" +) + +type apiMethodAwaitingApproval struct { + db *sql.DB +} + +func (vars *apiMethodAwaitingApproval) ServeHTTP(w http.ResponseWriter, req *http.Request) { + fields, hErr := unpackFieldParam(req.FormValue("fields"), + []string{"ipAddress", "reverseDns", "hostname", "received"}) + if hErr != nil { + http.Error(w, hErr.message, hErr.code) + return + } + + rows, err := vars.db.Query("SELECT ipaddr, hostname, received " + + "FROM waiting_for_approval WHERE approved IS NULL ORDER BY hostname") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer rows.Close() + + result := make([]map[string]interface{}, 0) + for rows.Next() { + var ipaddress, hostname sql.NullString + var received pq.NullTime + err = rows.Scan(&ipaddress, &hostname, &received) + if err != nil && err != sql.ErrNoRows { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + item := make(map[string]interface{}) + if fields["ipaddress"] { + item["ipAddress"] = ipaddress.String + } + if fields["hostname"] { + item["hostname"] = hostname.String + } + if fields["received"] { + item["received"] = jsonTime(received.Time) + } + if fields["reversedns"] { + var r string + if ipaddress.Valid { + // Reverse DNS lookup + names, err := net.LookupAddr(ipaddress.String) + if err == nil && len(names) > 0 { + r = strings.TrimRight(names[0], ".") + } + } + item["reverseDns"] = r + } + result = append(result, item) + } + + type Wrapper struct { + A []map[string]interface{} `json:"awaitingApproval"` + } + returnJSON(w, req, Wrapper{A: result}) +} diff --git a/server/jobrunner/api_file.go b/server/jobrunner/api_file.go new file mode 100644 index 0000000..ded7830 --- /dev/null +++ b/server/jobrunner/api_file.go @@ -0,0 +1,84 @@ +package main + +import ( + "database/sql" + "net/http" + "strconv" + + "github.com/lib/pq" +) + +type apiMethodFile struct { + db *sql.DB +} + +func (vars *apiMethodFile) ServeHTTP(w http.ResponseWriter, req *http.Request) { + fields, hErr := unpackFieldParam(req.FormValue("fields"), + []string{"fileId", "filename", "isCommand", "lastModified", "received", + "content", "certfp", "hostname"}) + if hErr != nil { + http.Error(w, hErr.message, hErr.code) + return + } + + if req.FormValue("fileId") == "" { + http.Error(w, "Missing parameter: fileId", http.StatusUnprocessableEntity) + return + } + fileid, err := strconv.Atoi(req.FormValue("fileId")) + if err != nil { + http.Error(w, "Unable to parse fileId", http.StatusBadRequest) + return + } + + //TODO fileId -> fileID ?? les google standard + + statement := "SELECT fileid,filename,is_command,mtime,received,content," + + "certfp,hostname FROM files f " + + "LEFT JOIN hostinfo h USING (certfp) " + + "WHERE fileid=$1" + rows, err := vars.db.Query(statement, fileid) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer rows.Close() + + if rows.Next() { + var fileID int + var filename, content, certfp, hostname sql.NullString + var isCommand sql.NullBool + var mtime, rtime pq.NullTime + rows.Scan(&fileID, &filename, &isCommand, &mtime, &rtime, &content, + &certfp, &hostname) + res := make(map[string]interface{}, 0) + if fields["fileid"] { + res["fileId"] = fileID + } + if fields["filename"] { + res["filename"] = filename.String + } + if fields["iscommand"] { + res["isCommand"] = isCommand.Bool + } + if fields["lastmodified"] { + res["lastModified"] = jsonTime(mtime.Time) + } + if fields["received"] { + res["received"] = jsonTime(rtime.Time) + } + if fields["content"] { + res["content"] = content.String + } + if fields["certfp"] { + res["certfp"] = certfp.String + } + if fields["hostname"] { + res["hostname"] = hostname.String + } + returnJSON(w, req, res) + } else { + // No file found. Return a "not found" status instead + http.Error(w, "File not found.", http.StatusNotFound) + } +} diff --git a/server/jobrunner/taskrunner.go b/server/jobrunner/taskrunner.go index 160e635..5325cdd 100644 --- a/server/jobrunner/taskrunner.go +++ b/server/jobrunner/taskrunner.go @@ -63,12 +63,19 @@ func main() { signal.Notify(c, os.Interrupt, syscall.SIGTERM) <-c quit = true - log.Println("Quitting...") + log.Println("\rShutting down...") }() - defer log.Println("Quit.") + defer log.Println("Stopped.") log.Println("Starting up.") - db, err := sql.Open("postgres", "dbname=apache host=/var/run/postgresql") + // Connect to database + var dbConnectionString string + if len(os.Args) >= 2 && os.Args[1] == "--dev" { + dbConnectionString = "sslmode=disable host=/var/run/postgresql" + } else { + dbConnectionString = "dbname=apache host=/var/run/postgresql" + } + db, err := sql.Open("postgres", dbConnectionString) if err != nil { log.Fatal(err) } @@ -98,6 +105,8 @@ func main() { } } + go runAPI(db, 4040) + for !quit { // Run jobs for _, j := range jobs { diff --git a/server/website/js/index.js b/server/website/js/index.js index 7b94276..cdd542d 100644 --- a/server/website/js/index.js +++ b/server/website/js/index.js @@ -4,7 +4,9 @@ $(document).ready(function(){ APIcall("mockapi/systemstatus_data.json", "systemstatus", $('#placeholder_systemstatus')); - APIcall("mockapi/awaiting_approval.json", "awaiting_approval", +// APIcall("mockapi/awaiting_approval.json", "awaiting_approval", +// $('#placeholder_approval')); + APIcall("http://127.0.0.1:4040/api/v0/awaitingApproval?fields=hostname,reversedns,ipaddress", "awaiting_approval", $('#placeholder_approval')); APIcall("mockapi/latestnewmachines.json", "latestnewmachines", $('#placeholder_latestnewmachines')); diff --git a/server/website/mockapi/awaiting_approval.json b/server/website/mockapi/awaiting_approval.json index 7b8b0e0..073d7b6 100644 --- a/server/website/mockapi/awaiting_approval.json +++ b/server/website/mockapi/awaiting_approval.json @@ -1,14 +1,16 @@ { - "awaitingapproval": [ + "awaitingApproval": [ { - "ipaddress": "127.0.0.1", - "reversedns": "localhost", - "oshostname": "liksom.local" + "ipAddress": "127.0.0.1", + "reverseDns": "localhost", + "hostname": "liksom.local", + "received": "2018-02-13T08:18:08+01:00" }, { - "ipaddress": "129.240.202.63", - "reversedns": "callisto.uio.no", - "oshostname": "callisto.uio.no" + "ipAddress": "129.240.202.63", + "reverseDns": "callisto.uio.no", + "hostname": "guesswho.local", + "received": "2018-02-13T08:18:08+01:00" } ] } diff --git a/server/website/mockapi/browsefile.json b/server/website/mockapi/browsefile.json index 4148184..ff4eabe 100644 --- a/server/website/mockapi/browsefile.json +++ b/server/website/mockapi/browsefile.json @@ -1,7 +1,9 @@ { + "fileId": 1234, "filename": "/usr/sbin/dmidecode -t system", - "type": "command", - "lastmodified": "2018-02-08T13:00:22+01", + "isCommand": "true", + "lastModified": "2018-02-08T13:00:22+01", + "received": "2018-02-08T13:11:45+01", "content": "Line one\nLine two\nLine three", "certfp": 817263, "hostname": "flyndre.example.no" diff --git a/server/website/mockapi/browsehost.json b/server/website/mockapi/browsehost.json index 0590f95..2bf7b6b 100644 --- a/server/website/mockapi/browsehost.json +++ b/server/website/mockapi/browsehost.json @@ -1,31 +1,33 @@ { "details": { - "ipaddress": "123.123.123.123", + "ipAddress": "123.123.123.123", "hostname": "flyndre.example.no", "lastseen": "2018-02-08T11:22:33+01", "os": "Fedora 27", - "os_edition": "Workstation", + "osEdition": "Workstation", "kernel": "4.14.16-300.fc27.x86_64", "vendor": "Dell Inc.", "model": "OptiPlex 7010", "serial": "1YF2YF3", "certfp": "298376ACBDFCBAFDC589356864ABCDEF", - "clientversion": "0.1.2" + "clientVersion": "0.1.2" }, "files": [ { "filename": "/etc/redhat-release", - "fileid": 123 + "fileId": 123 } ], "commands": [ { "filename": "/bin/uname -a", - "fileid": 234 + "fileId": 234 }, { "filename": "/usr/sbin/dmidecode -t system", - "fileid": 345 + "fileId": 345 } + ], + "support": [ ] } diff --git a/server/website/templates/awaiting_approval.handlebars b/server/website/templates/awaiting_approval.handlebars index e0fd4b1..5693250 100644 --- a/server/website/templates/awaiting_approval.handlebars +++ b/server/website/templates/awaiting_approval.handlebars @@ -1,11 +1,11 @@ -{{#if awaitingapproval}} +{{#if awaitingApproval}}

Waiting for approval

These machines are from ip address ranges not covered by the ruleset, and must be manually approved/denied.

-{{#each awaitingapproval}} +{{#each awaitingApproval}}

@@ -16,13 +16,13 @@

- + - + - + + - +
IP address{{ipaddress}}
{{ipAddress}}
Reverse DNS{{reversedns}}
{{reverseDns}}
Hostname from OS{{oshostname}}
{{hostname}}
Name in Nivlheim -
Kernel{{kernel}}
Hardware vendor{{vendor}}
Hardware model{{model}}
Serial number{{serial}}
Serial number{{serialNo}}
Certificate fingerprint{{certfp}}
Client version{{clientVersion}}
@@ -18,15 +18,21 @@

Files

{{#each files}} -{{filename}}
+{{#unless isCommand}} + + {{filename}}
+{{/unless}} {{/each}}

Commands

-{{#each commands}} -{{filename}}
+{{#each files}} +{{#if isCommand}} + + {{filename}}
+{{/if}} {{/each}}

From 38877f06103b6d75664d5daf402ead2fc32e8fed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Hagberg?= Date: Wed, 21 Feb 2018 13:30:19 +0100 Subject: [PATCH 07/16] API and website are mostly done New API functions: searchpage, status, awaitingApproval Hostlist-API supports wildcards, comparison, sorting, limit Unit tests for hostlist-API Handle CORS http headers and preflight for local development Website has lots of new functionality --- server/init.sql | 1 + server/jobrunner/api.go | 29 +- server/jobrunner/api.txt | 17 +- server/jobrunner/api_awaitingApproval.go | 68 +++- server/jobrunner/api_hostlist.go | 298 ++++++++++++++---- server/jobrunner/api_hostlist_test.go | 87 ++++- server/jobrunner/api_searchpage.go | 164 ++++++++++ server/jobrunner/api_status.go | 39 +++ server/website/browse.html | 2 +- server/website/index.html | 7 +- server/website/js/browse.js | 71 +++-- server/website/js/index.js | 47 ++- server/website/js/search.js | 39 ++- server/website/mockapi/awaiting_approval.json | 2 + server/website/mockapi/latestnewmachines.json | 16 +- server/website/mockapi/searchpage.json | 19 +- server/website/mockapi/systemstatus_data.json | 2 +- server/website/search.html | 9 +- server/website/style.css | 4 +- .../templates/awaiting_approval.handlebars | 7 +- .../website/templates/browsefile.handlebars | 2 +- .../website/templates/browsehost.handlebars | 2 +- .../templates/latestnewmachines.handlebars | 4 +- server/website/templates/search.handlebars | 18 +- .../website/templates/systemstatus.handlebars | 6 +- 25 files changed, 802 insertions(+), 158 deletions(-) create mode 100644 server/jobrunner/api_searchpage.go create mode 100644 server/jobrunner/api_status.go diff --git a/server/init.sql b/server/init.sql index 3ab5bda..a115a10 100644 --- a/server/init.sql +++ b/server/init.sql @@ -1,4 +1,5 @@ CREATE TABLE IF NOT EXISTS waiting_for_approval( + approvalid serial PRIMARY KEY NOT NULL, ipaddr text, hostname text, received timestamp with time zone, diff --git a/server/jobrunner/api.go b/server/jobrunner/api.go index bef32ec..1746476 100644 --- a/server/jobrunner/api.go +++ b/server/jobrunner/api.go @@ -17,12 +17,15 @@ import ( func runAPI(theDB *sql.DB, port int, devmode bool) { mux := http.NewServeMux() mux.Handle("/api/v0/awaitingApproval", &apiMethodAwaitingApproval{db: theDB}) + mux.Handle("/api/v0/awaitingApproval/", &apiMethodAwaitingApproval{db: theDB}) mux.Handle("/api/v0/file", &apiMethodFile{db: theDB}) mux.Handle("/api/v0/host", &apiMethodHost{db: theDB}) - mux.Handle("/api/v0/hostlist", &apiMethodHostList{db: theDB}) + mux.Handle("/api/v0/hostlist", &apiMethodHostList{db: theDB, devmode: devmode}) + mux.Handle("/api/v0/searchpage", &apiMethodSearchPage{db: theDB}) + mux.Handle("/api/v0/status", &apiMethodStatus{db: theDB}) var h http.Handler = mux if devmode { - h = wrapLog(wrapAccessControlAllowOrigin(h)) + h = wrapLog(wrapAllowLocalhostCORS(h)) } log.Printf("Serving API requests on localhost:%d\n", port) err := http.ListenAndServe(fmt.Sprintf("localhost:%d", port), h) @@ -47,17 +50,26 @@ func returnJSON(w http.ResponseWriter, req *http.Request, data interface{}) { } // For requests originating from localhost (typically on another port), -// this wrapper adds an http header that allows that origin. +// this wrapper adds http headers that allow that origin. // This makes it easier to test locally when developing. -func wrapAccessControlAllowOrigin(h http.Handler) http.Handler { +func wrapAllowLocalhostCORS(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { match, err := regexp.MatchString("http://(127\\.0\\.0\\.1|localhost)", req.Header.Get("Origin")) if match { w.Header().Set("Access-Control-Allow-Origin", req.Header.Get("Origin")) + w.Header().Set("Access-Control-Allow-Methods", + "GET, POST, HEAD, OPTIONS, PUT, DELETE, PATCH") + w.Header().Set("Vary", "Origin") } else if err != nil { log.Println(err) } + if req.Method == "OPTIONS" { + // When cross-domain, browsers sends OPTIONS first, to check for CORS headers + // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS + http.Error(w, "", http.StatusNoContent) + return + } h.ServeHTTP(w, req) }) } @@ -129,3 +141,12 @@ func unpackFieldParam(fieldParam string, allowedFields []string) (map[string]boo } return fields, nil } + +func contains(needle string, haystack []string) bool { + for _, s := range haystack { + if s == needle { + return true + } + } + return false +} diff --git a/server/jobrunner/api.txt b/server/jobrunner/api.txt index 0dfbadd..97f8e49 100644 --- a/server/jobrunner/api.txt +++ b/server/jobrunner/api.txt @@ -1,5 +1,10 @@ Liste over maskiner som venter på manuell godkjenning: -/api/v0/awaitingapproval?fields=ipAddress,reverseDns,osHostname +/api/v0/awaitingApproval?fields=ipAddress,reverseDns,hostname + +Godkjenne en maskin: +PUT /api/v0/awaitingApproval/?hostname= +Avvise en maskin: +DELETE /api/v0/awaitingApproval/ Hente en bestemt versjon av en fil: /api/v0/file?fileid=1234&fields=... @@ -23,11 +28,13 @@ host returnerer 404 hvis ingen maskin passer til kriteriene. (certfp eller hostn Liste alle maskiner som kjører Fedora: /api/v0/hostlist?os=Fedora*&fields=hostname -andre varianter: -/api/v0/hostlist?os=!Fedora*&fields=hostname + Operatorer: = != < > + Wildcards: * + lastseen-verdier: 12s 12m 12h 12d betyr "for så lenge siden" + Andre felter: limit offset [r]sort(default hostname) Søke i filer (fra GUI): -/api/v0/searchpage?q=søkeord&limit=10&offset=0&excerpt=80 +/api/v0/searchpage?q=søkefrase&page=1&excerpt=80&hitsPerPage=10 Søke i filer for scripts: -/api/v0/search? +/api/v0/search?q=søkefrase&fields=hostname,filename diff --git a/server/jobrunner/api_awaitingApproval.go b/server/jobrunner/api_awaitingApproval.go index 588464f..c3dfbe1 100644 --- a/server/jobrunner/api_awaitingApproval.go +++ b/server/jobrunner/api_awaitingApproval.go @@ -2,8 +2,11 @@ package main import ( "database/sql" + "fmt" "net" "net/http" + "regexp" + "strconv" "strings" "github.com/lib/pq" @@ -14,14 +17,19 @@ type apiMethodAwaitingApproval struct { } func (vars *apiMethodAwaitingApproval) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if req.Method != "GET" { + vars.ServeHTTPREST(w, req) + return + } + fields, hErr := unpackFieldParam(req.FormValue("fields"), - []string{"ipAddress", "reverseDns", "hostname", "received"}) + []string{"ipAddress", "reverseDns", "hostname", "received", "approvalId"}) if hErr != nil { http.Error(w, hErr.message, hErr.code) return } - rows, err := vars.db.Query("SELECT ipaddr, hostname, received " + + rows, err := vars.db.Query("SELECT ipaddr, hostname, received, approvalId " + "FROM waiting_for_approval WHERE approved IS NULL ORDER BY hostname") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -31,16 +39,20 @@ func (vars *apiMethodAwaitingApproval) ServeHTTP(w http.ResponseWriter, req *htt result := make([]map[string]interface{}, 0) for rows.Next() { + var approvalID int var ipaddress, hostname sql.NullString var received pq.NullTime - err = rows.Scan(&ipaddress, &hostname, &received) + err = rows.Scan(&ipaddress, &hostname, &received, &approvalID) if err != nil && err != sql.ErrNoRows { http.Error(w, err.Error(), http.StatusInternalServerError) return } item := make(map[string]interface{}) - if fields["ipaddress"] { + if fields["approvalId"] { + item["approvalId"] = approvalID + } + if fields["ipAddress"] { item["ipAddress"] = jsonString(ipaddress) } if fields["hostname"] { @@ -49,7 +61,7 @@ func (vars *apiMethodAwaitingApproval) ServeHTTP(w http.ResponseWriter, req *htt if fields["received"] { item["received"] = jsonTime(received) } - if fields["reversedns"] { + if fields["reverseDns"] { var r string if ipaddress.Valid { // Reverse DNS lookup @@ -68,3 +80,49 @@ func (vars *apiMethodAwaitingApproval) ServeHTTP(w http.ResponseWriter, req *htt } returnJSON(w, req, Wrapper{A: result}) } + +func (vars *apiMethodAwaitingApproval) ServeHTTPREST(w http.ResponseWriter, + req *http.Request) { + var approved bool + switch req.Method { + case "PUT": + approved = true + case "DELETE": + approved = false + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + match := regexp.MustCompile("/(\\d+)$").FindStringSubmatch(req.URL.Path) + if match == nil { + http.Error(w, "Missing approvalId in URL path", http.StatusUnprocessableEntity) + return + } + approvalID, _ := strconv.Atoi(match[1]) + + var hostname string + var res sql.Result + var err error + if approved { + if hostname = req.FormValue("hostname"); hostname == "" { + http.Error(w, "Missing parameter: hostname", http.StatusUnprocessableEntity) + return + } + res, err = vars.db.Exec("UPDATE waiting_for_approval SET approved=true, "+ + " hostname=$1 WHERE approvalId=$2", hostname, approvalID) + } else { + res, err = vars.db.Exec("UPDATE waiting_for_approval SET approved=false "+ + "WHERE approvalId=$1", approvalID) + } + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if rows, err := res.RowsAffected(); rows == 0 || err != nil { + http.Error(w, fmt.Sprintf("Entity with id %d not found ", approvalID), + http.StatusNotFound) + return + } + http.Error(w, "", http.StatusNoContent) +} diff --git a/server/jobrunner/api_hostlist.go b/server/jobrunner/api_hostlist.go index 46d21ec..691baf1 100644 --- a/server/jobrunner/api_hostlist.go +++ b/server/jobrunner/api_hostlist.go @@ -3,13 +3,26 @@ package main import ( "database/sql" "fmt" + "log" "net/http" "net/url" + "regexp" + "strconv" "strings" + + "github.com/lib/pq" ) type apiMethodHostList struct { - db *sql.DB + db *sql.DB + devmode bool +} + +var hostInfoDbFieldNames = map[string]string{ + "ipAddress": "ipaddr", + "osEdition": "os_edition", + "serialNo": "serialno", + "clientVersion": "clientversion", } var apiHostListSourceFields = []string{"ipAddress", "hostname", "lastseen", "os", "osEdition", @@ -17,82 +30,253 @@ var apiHostListSourceFields = []string{"ipAddress", "hostname", "lastseen", "os" "clientVersion"} func (vars *apiMethodHostList) ServeHTTP(w http.ResponseWriter, req *http.Request) { - /* - fields, hErr := unpackFieldParam(req.FormValue("fields"), - apiHostListSourceFields) - if hErr != nil { - http.Error(w, hErr.message, hErr.code) - return - } - */ + fields, hErr := unpackFieldParam(req.FormValue("fields"), + apiHostListSourceFields) + if hErr != nil { + http.Error(w, hErr.message, hErr.code) + return + } + err := req.ParseForm() if err != nil { - http.Error(w, err.Error(), 400) + http.Error(w, err.Error(), http.StatusBadRequest) return } - where, qparams := buildSQLWhere(&apiHostListSourceFields, &req.Form) + where, qparams, hErr := buildSQLWhere(req.URL.RawQuery) + if hErr != nil { + http.Error(w, hErr.message, hErr.code) + return + } + + statement := "SELECT ipaddr, hostname, lastseen, os, os_edition, " + + "kernel, vendor, model, serialno, certfp, clientversion " + + "FROM hostinfo " + if len(where) > 0 { + statement += " WHERE " + where + } - fmt.Fprintln(w, where) - for i, v := range qparams { - fmt.Fprintf(w, "$%d = %s\n", i+1, v) + var desc string + sort := req.FormValue("sort") + if sort == "" { + sort = req.FormValue("rsort") + if sort != "" { + desc = "DESC" + } + } + if sort == "" { + sort = "hostname" + } else if contains(sort, apiHostListSourceFields) { + h, ok := hostInfoDbFieldNames[sort] + if ok { + sort = h + } + } else { + http.Error(w, "Unsupported sort field", http.StatusUnprocessableEntity) + return + } + statement += fmt.Sprintf(" ORDER BY %s %s", sort, desc) + + if req.FormValue("limit") != "" { + var limit int + if limit, err = strconv.Atoi(req.FormValue("limit")); err == nil { + statement += fmt.Sprintf(" LIMIT %d", limit) + } else { + http.Error(w, "Invalid limit value", http.StatusBadRequest) + return + } + } + if req.FormValue("offset") != "" { + var offset int + if offset, err = strconv.Atoi(req.FormValue("offset")); err == nil { + statement += fmt.Sprintf(" OFFSET %d", offset) + } else { + http.Error(w, "Invalid offset value", http.StatusBadRequest) + return + } + } + + if vars.devmode { + log.Println(statement) + log.Print(qparams) + } + + rows, err := vars.db.Query(statement, qparams...) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer rows.Close() + result := make([]map[string]interface{}, 0) + for rows.Next() { + var ipaddr, hostname, os, osEdition, kernel, vendor, + model, serialNo, certfp, clientversion sql.NullString + var lastseen pq.NullTime + err = rows.Scan(&ipaddr, &hostname, &lastseen, &os, &osEdition, + &kernel, &vendor, &model, &serialNo, &certfp, &clientversion) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + res := make(map[string]interface{}, 0) + if fields["ipAddress"] { + res["ipAddress"] = jsonString(ipaddr) + } + if fields["hostname"] { + res["hostname"] = jsonString(hostname) + } + if fields["lastseen"] { + res["lastseen"] = jsonTime(lastseen) + } + if fields["os"] { + res["os"] = jsonString(os) + } + if fields["osEdition"] { + res["osEdition"] = jsonString(osEdition) + } + if fields["kernel"] { + res["kernel"] = jsonString(kernel) + } + if fields["vendor"] { + res["vendor"] = jsonString(vendor) + } + if fields["model"] { + res["model"] = jsonString(model) + } + if fields["serialNo"] { + res["serialNo"] = jsonString(serialNo) + } + if fields["certfp"] { + res["certfp"] = jsonString(certfp) + } + if fields["clientVersion"] { + res["clientVersion"] = jsonString(clientversion) + } + result = append(result, res) + } + if err = rows.Err(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return } + returnJSON(w, req, result) } // Build the WHERE part of the SQL statement based on parameters. -// Must support "*" as a wildcard, and if a value starts with "!" -// it should be interpreted as NOT. -func buildSQLWhere(fields *[]string, form *url.Values) (string, []interface{}) { - dbFieldNames := map[string]string{ - "ipAddress": "ipaddr", - "osEdition": "os_edition", - "serialNo": "serialno", - "clientVersion": "client_version", - } +// - Supports "*" as a wildcard +// - If a value starts with "!" it means not equal to or not like +// - If a value starts with "<" or ">" it affects the comparison +func buildSQLWhere(queryString string) (string, []interface{}, *httpError) { + // This slice will hold multiple clauses that will be ANDed together after where := make([]string, 0) + // This slice will hold parameter values for the query qparams := make([]interface{}, 0) - // Here, the "fields" map is also used as a list of fields that - // the user can supply filter values for. - for _, key := range *fields { - for _, value := range (*form)[key] { - not := ' ' - if len(value) > 0 && value[0] == '!' { - not = '!' - value = value[1:] + + re := regexp.MustCompile("^(\\w+)([=!<>]{1,2})([\\w,\\*%]+)$") + for _, pair := range strings.Split(queryString, "&") { + un, err := url.QueryUnescape(pair) + if err == nil { + pair = un + } + m := re.FindStringSubmatch(pair) + if m == nil || err != nil { + return "", nil, &httpError{ + code: http.StatusBadRequest, + message: "Syntax error: " + pair, } - name, ok := dbFieldNames[key] - if !ok { - name = key + } + name := m[1] + if name == "fields" || name == "sort" || name == "rsort" || + name == "limit" || name == "offset" { + continue + } + operator := m[2] + ok := false + for _, s := range []string{"=", "!=", "<", ">"} { + if s == operator { + ok = true + break } - if strings.Index(value, "*") > -1 { - // The value has wildcards - parts := make([]string, 0) - for _, valuePart := range strings.Split(value, "*") { - if len(valuePart) > 0 { - qparams = append(qparams, valuePart) - parts = append(parts, fmt.Sprintf("$%d", len(qparams))) - } - } - joined := strings.Join(parts, "||'%'||") - if strings.HasPrefix(value, "*") { - joined = "'%'||" + joined + } + if !ok { + return "", nil, &httpError{ + code: http.StatusBadRequest, + message: "Unsupported operator: " + operator, + } + } + value := m[3] + validFieldName := false + for _, key := range apiHostListSourceFields { + if strings.EqualFold(key, name) { + validFieldName = true + break + } + } + if !validFieldName { + return "", nil, &httpError{ + message: "Unsupported field name: " + name, + code: http.StatusUnprocessableEntity, + } + } + // the name of the field in the database + dbname, ok := hostInfoDbFieldNames[name] + if !ok { + dbname = name + } + // Wildcards? + if strings.Index(value, "*") > -1 { + // The value contains wildcards + parts := make([]string, 0) + for _, valuePart := range strings.Split(value, "*") { + if len(valuePart) > 0 { + qparams = append(qparams, valuePart) + parts = append(parts, fmt.Sprintf("$%d", len(qparams))) } - if strings.HasSuffix(value, "*") { - joined += "||'%'" + } + joined := strings.Join(parts, "||'%'||") + if strings.HasPrefix(value, "*") { + joined = "'%'||" + joined + } + if strings.HasSuffix(value, "*") { + joined += "||'%'" + } + if operator == "!=" { + where = append(where, fmt.Sprintf("%s NOT LIKE %s", + dbname, joined)) + } else if operator == "=" { + where = append(where, fmt.Sprintf("%s LIKE %s", + dbname, joined)) + } else { + return "", nil, &httpError{ + message: "Can't use operator '" + operator + "' with wildcards ('*')", + code: http.StatusBadRequest, } - if not == '!' { - where = append(where, fmt.Sprintf("%s NOT LIKE %s", - name, joined)) - } else { - where = append(where, fmt.Sprintf("%s LIKE %s", - name, joined)) + } + } else { + // The value doesn't contain wildcards. + if name == "lastseen" { + // lastseen relative time magic. Examples: + // >2h = more than 2 hours ago + // <30m = less than 30 minutes ago + // supported time units: s(seconds), m(minutes), h(hours), d(days) + var count int + var unit string + _, err := fmt.Sscanf(value, "%d%s", &count, &unit) + if err != nil || len(unit) > 1 { + return "", nil, &httpError{ + message: "Wrong format for lastseen parameter", + code: http.StatusBadRequest, + } } + where = append(where, + fmt.Sprintf("now()-interval '%d%s' %s lastseen", + count, unit, operator)) } else { qparams = append(qparams, value) - where = append(where, fmt.Sprintf("%s%c= $%d", name, not, - len(qparams))) + where = append(where, fmt.Sprintf("%s %s $%d", dbname, + operator, len(qparams))) } } } - return strings.Join(where, " AND "), qparams + sql := strings.Join(where, " AND ") + return sql, qparams, nil } diff --git a/server/jobrunner/api_hostlist_test.go b/server/jobrunner/api_hostlist_test.go index a92dede..6f2cf07 100644 --- a/server/jobrunner/api_hostlist_test.go +++ b/server/jobrunner/api_hostlist_test.go @@ -1,21 +1,88 @@ package main import ( - "net/url" "reflect" "testing" ) func TestBuildSQLWhere(t *testing.T) { - values := make(url.Values) - values.Set("hostname", "a*b*c") - result, params := buildSQLWhere(&apiHostListSourceFields, &values) - expected := "hostname LIKE $1||'%'||$2||'%'||$3" - if expected != result { - t.Fatalf("Wrong SQL. Got %s, expected %s", result, expected) + type whereTest struct { + query string + sql string + params []interface{} + errmsg string } - expectedParams := []interface{}{"a", "b", "c"} - if !reflect.DeepEqual(expectedParams, params) { - t.Fatalf("Wrong SQL params. Got %v, expected %v", params, expectedParams) + tests := []whereTest{ + whereTest{ + query: "hostname=a*b*c&hostname!=*dd*ee*", + sql: "hostname LIKE $1||'%'||$2||'%'||$3 AND " + + "hostname NOT LIKE '%'||$4||'%'||$5||'%'", + params: []interface{}{"a", "b", "c", "dd", "ee"}, + }, + whereTest{ + query: "os=Fedora&vendor!=Dell*", + sql: "os = $1 AND vendor NOT LIKE $2||'%'", + params: []interface{}{"Fedora", "Dell"}, + }, + whereTest{ + query: "fields=hostname,ipaddress&lastseen<2h", + sql: "now()-interval '2h' < lastseen", + params: []interface{}{}, + }, + whereTest{ + query: "lastseen<2mdb", + sql: "", + params: nil, + errmsg: "Wrong format for lastseen parameter", + }, + whereTest{ + query: "ipaddress$%#!", + sql: "", + params: nil, + errmsg: "Syntax error: ipaddress$%#!", + }, + whereTest{ + query: "nonexistentfield=123", + sql: "", + params: nil, + errmsg: "Unsupported field name: nonexistentfield", + }, + whereTest{ + query: "hostname>orange*", + sql: "", + params: nil, + errmsg: "Can't use operator '>' with wildcards ('*')", + }, + whereTest{ + query: "hostname=>foo", + sql: "", + params: nil, + errmsg: "Unsupported operator: =>", + }, + } + + for _, w := range tests { + result, params, err := buildSQLWhere(w.query) + if err != nil && err.message != w.errmsg { + if w.errmsg != "" { + t.Errorf("Wrong error message.\n Got: %s\n"+ + "Expected: %s", err.message, w.errmsg) + continue + } else { + // Unexpected error + t.Errorf("%s\n%s", err.message, w.query) + continue + } + } else if w.errmsg != "" && err == nil { + t.Errorf("Expected error \"%s\", got success.\n%s", w.errmsg, w.query) + continue + } + if w.sql != result { + t.Errorf("Wrong SQL.\n Got: %s\n"+ + "Expected: %s", result, w.sql) + } + if !reflect.DeepEqual(w.params, params) { + t.Errorf("Wrong SQL params. Got %v, expected %v", params, w.params) + } } } diff --git a/server/jobrunner/api_searchpage.go b/server/jobrunner/api_searchpage.go new file mode 100644 index 0000000..51bd14b --- /dev/null +++ b/server/jobrunner/api_searchpage.go @@ -0,0 +1,164 @@ +package main + +import ( + "database/sql" + "encoding/json" + "html" + "log" + "net/http" + "strconv" + "strings" +) + +type apiMethodSearchPage struct { + db *sql.DB +} + +type apiSearchPageHit struct { + FileID int `json:"fileId"` + Filename jsonString `json:"filename"` + IsCommand bool `json:"isCommand"` + Excerpt string `json:"excerpt"` + Hostname jsonString `json:"hostname"` + CertFP jsonString `json:"certfp"` + DisplayNumber int `json:"displayNumber"` +} + +type apiSearchPageResult struct { + Query string `json:"query"` + NumHits int `json:"numberOfHits"` + Page int `json:"page"` + Hits []apiSearchPageHit `json:"hits"` +} + +func (vars *apiMethodSearchPage) ServeHTTP(w http.ResponseWriter, req *http.Request) { + result := new(apiSearchPageResult) + err := req.ParseForm() + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + _, ok := req.Form["q"] + if !ok { + http.Error(w, "Missing parameter: q", http.StatusUnprocessableEntity) + return + } + result.Query = req.FormValue("q") + if result.Query == "" { + result.Page = 1 + result.NumHits = 0 + result.Hits = make([]apiSearchPageHit, 0) + returnJSON(w, req, result) + return + } + + result.Page, err = strconv.Atoi(req.FormValue("page")) + if err != nil { + result.Page = 1 + } + + row, err := vars.db.Query( + "SELECT count(*) FROM (SELECT content,row_number() OVER "+ + "(PARTITION BY certfp,filename ORDER BY mtime DESC) "+ + "FROM files) AS foo "+ + "WHERE row_number=1 AND content ILIKE '%'||$1||'%'", + result.Query) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer row.Close() + if row.Next() { + err = row.Scan(&result.NumHits) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + + var pageSize = 10 + if req.FormValue("hitsPerPage") != "" { + var ps int + if ps, err = strconv.Atoi(req.FormValue("hitsPerPage")); err == nil { + pageSize = ps + } else { + http.Error(w, "Invalid hitsPerPage value", http.StatusBadRequest) + return + } + } + + st := "SELECT fileid,filename,is_command,hostname,certfp,content " + + "FROM (SELECT fileid,filename,is_command,certfp,content," + + "row_number() OVER (PARTITION BY certfp,filename ORDER BY mtime DESC) " + + "FROM files) AS foo LEFT JOIN hostinfo USING (certfp) " + + "WHERE row_number=1 AND content ILIKE '%'||$1||'%' " + + "ORDER BY hostname LIMIT $2 OFFSET $3" + + rows, err := vars.db.Query(st, result.Query, pageSize, + (result.Page-1)*pageSize) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer rows.Close() + + lq := strings.ToLower(html.EscapeString(result.Query)) + exSize, err := strconv.Atoi(req.FormValue("excerpt")) + if err != nil { + exSize = 50 + } + + result.Hits = make([]apiSearchPageHit, 0) + for displayNumber := 1; rows.Next(); displayNumber++ { + var filename, hostname, certfp, content sql.NullString + var isCommand sql.NullBool + hit := apiSearchPageHit{} + err = rows.Scan(&hit.FileID, &filename, &isCommand, &hostname, &certfp, + &content) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + hit.Filename = jsonString(filename) + hit.Hostname = jsonString(hostname) + hit.CertFP = jsonString(certfp) + hit.IsCommand = isCommand.Bool + hit.DisplayNumber = displayNumber + // must html-escape the content excerpt + ec := html.EscapeString(content.String) + i := strings.Index(strings.ToLower(ec), lq) + start := i - exSize/2 + if start < 0 { + start = 0 + } + end := i + exSize/2 + if end > len(ec) { + end = len(ec) + } + ex := ec[start:end] + i = strings.Index(strings.ToLower(ex), lq) + i2 := i + len(lq) + ex = ex[0:i2] + "" + ex[i2:] + ex = ex[0:i] + "" + ex[i:] + if len(ex) > 0 && ex[len(ex)-1] != ' ' { + ex += "…" + } + hit.Excerpt = ex + result.Hits = append(result.Hits, hit) + } + if err = rows.Err(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + jsonEnc := json.NewEncoder(w) + jsonEnc.SetEscapeHTML(false) + jsonEnc.SetIndent("", " ") + err = jsonEnc.Encode(result) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Println(err.Error()) + return + } +} diff --git a/server/jobrunner/api_status.go b/server/jobrunner/api_status.go new file mode 100644 index 0000000..253e614 --- /dev/null +++ b/server/jobrunner/api_status.go @@ -0,0 +1,39 @@ +package main + +import ( + "database/sql" + "net/http" +) + +type apiMethodStatus struct { + db *sql.DB +} + +func (vars *apiMethodStatus) ServeHTTP(w http.ResponseWriter, req *http.Request) { + type Status struct { + FilesLastHour int `json:"filesLastHour"` + NumOfMachines int `json:"numberOfMachines"` + ReportingPercentageLastHour int `json:"reportingPercentageLastHour"` + } + status := Status{} + + var machinesLastHour int + + err := vars.db.QueryRow("SELECT count(*), count(distinct(certfp)) "+ + "FROM files WHERE received > now() - interval '1 hour'"). + Scan(&status.FilesLastHour, &machinesLastHour) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = vars.db.QueryRow("SELECT count(*) FROM hostinfo"). + Scan(&status.NumOfMachines) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + status.ReportingPercentageLastHour = 100 * machinesLastHour / status.NumOfMachines + + returnJSON(w, req, status) +} diff --git a/server/website/browse.html b/server/website/browse.html index 4ae89d8..51810d9 100644 --- a/server/website/browse.html +++ b/server/website/browse.html @@ -3,7 +3,7 @@ - Hello UiO! + Nivlheim - + + diff --git a/server/website/js/browse.js b/server/website/js/browse.js index f9135c1..7eddec2 100644 --- a/server/website/js/browse.js +++ b/server/website/js/browse.js @@ -1,17 +1,25 @@ -function browseHost(certfp) { +function browseHost(certfp, pushState = true) { + if (pushState) { + history.pushState({"certfp":certfp}, null, "/browse.html?c="+ + encodeURIComponent(certfp)); + } APIcall( //"mockapi/browsehost.json", - "http://127.0.0.1:4040/api/v0/host?certfp="+certfp+ + "http://127.0.0.1:4040/api/v0/host?certfp="+encodeURIComponent(certfp)+ "&fields=ipAddress,hostname,lastseen,os,osEdition,"+ "kernel,vendor,model,serialNo,clientVersion,certfp,files", "browsehost", "#placeholder_browse"); } -function browseFile(fileId) { +function browseFile(fileId, pushState = true) { + if (pushState) { + history.pushState({"fileId":fileId}, null, "/browse.html?f="+ + encodeURIComponent(fileId)); + } APIcall( //"mockapi/browsefile.json", "http://127.0.0.1:4040/api/v0/file?fields=lastModified,hostname,filename,"+ - "content,certfp,versions&fileId="+fileId, + "content,certfp,versions&fileId="+encodeURIComponent(fileId), "browsefile", "#placeholder_browse") .done(function(){ $("select#selectVersion").val(fileId); @@ -21,12 +29,21 @@ function browseFile(fileId) { }); } -function browseFile2(hostname, filename) { +function browseFile2(hostname, filename, pushState = true) { + if (pushState) { + history.pushState({ + "filename":filename, + "hostname":hostname + }, null, + "/browse.html?fn="+encodeURIComponent(filename)+ + "&h="+encodeURIComponent(hostname)); + } APIcall( //"mockapi/browsefile.json", "http://127.0.0.1:4040/api/v0/file?fields=fileId,lastModified,"+ "hostname,filename,content,certfp,versions"+ - "&filename="+filename+"&hostname="+hostname, + "&filename="+encodeURIComponent(filename)+ + "&hostname="+encodeURIComponent(hostname), "browsefile", "#placeholder_browse") .done(function(){ $("select#selectVersion:first-child").prop("selected","selected"); @@ -36,27 +53,39 @@ function browseFile2(hostname, filename) { }); } -function readyFunc() { - Handlebars.registerHelper('formatDateTime', function(s){ - var t = moment(s); - return t.fromNow() + ' (' + t.format('D MMM Y HH:mm') + ')'; - }); +function navigateByUrlParams() { var p = getUrlParams(); if (p['c']) { - browseHost(p['c']); + browseHost(p['c'], false); } else if (p['f']) { - browseFile(p['f']); + browseFile(p['f'], false); } else if (p['h'] && p['fn']) { - browseFile2(p['h'], p['fn']); + browseFile2(p['h'], p['fn'], false); } } -$(document).ready(readyFunc); +function readyFunc() { + Handlebars.registerHelper('formatDateTime', function(s){ + if (!s) return ""; + var t = moment(s); + return t.fromNow() + ' (' + t.format('D MMM Y HH:mm') + ')'; + }); + window.addEventListener('popstate', popstate); + navigateByUrlParams(); +} -/* this code doesn't work yet -- needs debugging -window.history.pushState(null, null, "/browse.html?f="+fileId); -window.onpopstate = function(event) { - console.log("popstate"); - readyFunc(); +function popstate(e) { + if (e.state) { + if (e.state.certfp) { + browseHost(e.state.certfp, false); + } else if (e.state.fileId) { + browseFile(e.state.fileId, false); + } else if (e.state.hostname) { + browseFile2(e.state.hostname, e.state.filename, false); + } + } else { + navigateByUrlParams(); + } } -*/ + +$(document).ready(readyFunc); diff --git a/server/website/js/index.js b/server/website/js/index.js index 5dfd273..b5b97a8 100644 --- a/server/website/js/index.js +++ b/server/website/js/index.js @@ -2,12 +2,49 @@ $(document).ready(function(){ p = getUrlParams(); $('input#search').val(p['q']); - APIcall("mockapi/systemstatus_data.json", "systemstatus", - $('#placeholder_systemstatus')); + Handlebars.registerHelper('formatDateTime', function(s){ + if (!s) return ""; + var t = moment(s); + return t.fromNow() + ' (' + t.format('D MMM Y HH:mm') + ')'; + }); + + APIcall( + //"mockapi/systemstatus_data.json", + "http://127.0.0.1:4040/api/v0/status", + "systemstatus", $('#placeholder_systemstatus')); APIcall( //"mockapi/awaiting_approval.json", - "http://127.0.0.1:4040/api/v0/awaitingApproval?fields=hostname,reversedns,ipaddress", + "http://127.0.0.1:4040/api/v0/awaitingApproval"+ + "?fields=hostname,reversedns,ipaddress,approvalId", "awaiting_approval", $('#placeholder_approval')); - APIcall("mockapi/latestnewmachines.json", "latestnewmachines", - $('#placeholder_latestnewmachines')); + APIcall( + //"mockapi/latestnewmachines.json", + "http://127.0.0.1:4040/api/v0/hostlist?fields=hostname,certfp,lastseen"+ + "&rsort=lastseen&limit=10", + "latestnewmachines", $('#placeholder_latestnewmachines')); }); + +function approve(id) { + $.ajax({ + url : 'http://127.0.0.1:4040/api/v0/awaitingApproval/' + +id+'?hostname='+$('input#hostname'+id).val(), + method: "PUT" + }) + .always(function(){ + APIcall("http://127.0.0.1:4040/api/v0/awaitingApproval"+ + "?fields=hostname,reversedns,ipaddress,approvalId", + "awaiting_approval", $('#placeholder_approval')); + }); +} + +function deny(id) { + $.ajax({ + url : 'http://127.0.0.1:4040/api/v0/awaitingApproval/'+id, + method: "DELETE" + }) + .always(function(){ + APIcall("http://127.0.0.1:4040/api/v0/awaitingApproval"+ + "?fields=hostname,reversedns,ipaddress,approvalId", + "awaiting_approval", $('#placeholder_approval')); + }); +} diff --git a/server/website/js/search.js b/server/website/js/search.js index 705b2cb..3974e11 100644 --- a/server/website/js/search.js +++ b/server/website/js/search.js @@ -1,16 +1,45 @@ -var spinnerhtml; +var spinnerhtml, query = ""; $(document).ready(function(){ + // copy the search string from the url parameter to the search input field var p = getUrlParams(); $('input#search').val(p['q']); + // make a copy of the spinner html spinnerhtml = $("#placeholder_searchresult").html(); - APIcall("mockapi/search.json", "search", "#placeholder_searchresult"); + // add event listener for back button + window.addEventListener('popstate', popstate); + // search + query = p['q']; + performSearch(query); }); -function newsearch() { +// This function is called when the user clicks "search" or presses enter +function newSearch() { // put the spinner back $('#placeholder_searchresult').html(spinnerhtml); + // fake the url + query = $('input#search').val(); + history.pushState({"query":query}, null, "/search.html?q="+query); // perform the new search - var q = $('input#search').val(); - APIcall("mockapi/search.json", "search", "#placeholder_searchresult"); + performSearch(query); +} + +function performSearch(q) { + APIcall( + //"mockapi/searchpage.json", + "http://127.0.0.1:4040/api/v0/searchpage?q="+encodeURIComponent(q)+ + "&page=1&hitsPerPage=10&excerpt=80", + "search", "#placeholder_searchresult"); +} + +function popstate(e) { + if (e.state) { + query = e.state.query; + } else { + var p = getUrlParams(); + query = p['q']; + } + $('input#search').val(query); + $('#placeholder_searchresult').html(spinnerhtml); + performSearch(query); } diff --git a/server/website/mockapi/awaiting_approval.json b/server/website/mockapi/awaiting_approval.json index 073d7b6..d64a1cb 100644 --- a/server/website/mockapi/awaiting_approval.json +++ b/server/website/mockapi/awaiting_approval.json @@ -1,12 +1,14 @@ { "awaitingApproval": [ { + "approvalId": 1, "ipAddress": "127.0.0.1", "reverseDns": "localhost", "hostname": "liksom.local", "received": "2018-02-13T08:18:08+01:00" }, { + "approvalId": 2, "ipAddress": "129.240.202.63", "reverseDns": "callisto.uio.no", "hostname": "guesswho.local", diff --git a/server/website/mockapi/latestnewmachines.json b/server/website/mockapi/latestnewmachines.json index 7b21de6..2a747a3 100644 --- a/server/website/mockapi/latestnewmachines.json +++ b/server/website/mockapi/latestnewmachines.json @@ -1,9 +1,7 @@ -{ - "latestnewmachines": [ - { - "hostname": "flyndre.example.no", - "certfp": "3C7D5CCE52C6949ABB57470FB0B25A22CA7DA78F", - "firstseen": "2018-02-04T13:23:34+01" - } - ] -} +[ + { + "hostname": "flyndre.example.no", + "certfp": "3C7D5CCE52C6949ABB57470FB0B25A22CA7DA78F", + "lastseen": "2018-02-04T13:23:34+01" + } +] diff --git a/server/website/mockapi/searchpage.json b/server/website/mockapi/searchpage.json index 9bb5656..0015869 100644 --- a/server/website/mockapi/searchpage.json +++ b/server/website/mockapi/searchpage.json @@ -1,33 +1,34 @@ { "query": "27", "numberOfHits": 3, + "page": 1, "hits": [ { - "fileid": 123, + "fileId": 123, "filename": "/etc/redhat-release", - "type": "file", + "isCommand": false, "excerpt": "Fedora release 27 (Twenty Seven)", "hostname": "flyndre", "certfp": "ABCD1234", - "number": 1 + "displayNumber": 1 }, { - "fileid": 456, + "fileId": 456, "filename": "/bin/uname -a", - "type": "command", + "isCommand": true, "excerpt": "yndre 4.14.14-300.fc27.x86_64 #1 SMP Fri J", "hostname": "flyndre", "certfp": "DEFE3232", - "number": 2 + "displayNumber": 2 }, { - "fileid": 789, + "fileId": 789, "filename": "/usr/sbin/dmidecode -t system", - "type": "command", + "isCommand": true, "excerpt": "0x0100, DMI type 1, 27 - Hello UiO! + Nivlheim - + + @@ -27,7 +28,7 @@
-
+
Hostname from OS {{hostname}} Name in Nivlheim - + - - - - -
-{{end}} -

-{{end}} - -{{if .machines}} -

-

Machines

-{{template "searchbox" .}} -{{range .machines}} -{{.}}
-{{end}} -

-{{end}} - - - - -{{define "logo"}} - -{{end}} diff --git a/server/templates/searchpage.html b/server/templates/searchpage.html deleted file mode 100644 index 24f4ac5..0000000 --- a/server/templates/searchpage.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - Nivlheim -{{template "common_head.html"}} - - -{{template "logo"}} -{{template "searchbox" .}} - -

-{{.count}} hits -

- -{{range .hits}} -

- - {{.Hostname}}: - - {{.Filename}}
- {{.Excerpt}} -

-{{end}} - - - - - -{{define "searchbox"}} - -{{end}} diff --git a/server/web/browse.go b/server/web/browse.go deleted file mode 100644 index d1b9bad..0000000 --- a/server/web/browse.go +++ /dev/null @@ -1,142 +0,0 @@ -package main - -import ( - "database/sql" - "html/template" - "net/http" - "strconv" - - "github.com/lib/pq" -) - -type Hostinfo struct { - Hostname sql.NullString - IPaddr sql.NullString - Certfp string - Kernel sql.NullString - Lastseen pq.NullTime - OS sql.NullString - OSEdition sql.NullString - Vendor sql.NullString - Model sql.NullString - Serialno sql.NullString - Clientversion sql.NullString -} - -type File struct { - Fileid int - Filename sql.NullString - Received pq.NullTime - Content sql.NullString -} - -func browse(w http.ResponseWriter, req *http.Request) { - db, err := sql.Open("postgres", dbConnectionString) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer db.Close() - - // Load templates - templates, err := template.ParseGlob(templatePath + "/*") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - tValues := make(map[string]interface{}) - var templatename string - - if (req.FormValue("f") != "" && req.FormValue("c") != "") || - req.FormValue("fid") != "" { - - templatename = "browsefile.html" - fileid, err := strconv.Atoi(req.FormValue("fid")) - if err != nil { - err = db.QueryRow("SELECT fileid FROM files "+ - "WHERE filename=$1 AND certfp=$2 "+ - "ORDER BY received DESC LIMIT 1", - req.FormValue("f"), req.FormValue("c")).Scan(&fileid) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - } - if fileid == 0 { - http.Error(w, "", http.StatusNotFound) - return - } - var f File - var hostname sql.NullString - err = db.QueryRow("SELECT fileid,content,filename,received,hostname "+ - "FROM files JOIN hostinfo ON hostinfo.certfp=files.certfp "+ - "WHERE fileid=$1", fileid). - Scan(&f.Fileid, &f.Content, &f.Filename, &f.Received, &hostname) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - tValues["file"] = f - if hostname.Valid { - tValues["hostname"] = hostname.String - } - } else if req.FormValue("h") != "" { - templatename = "browsehost.html" - - // Hostinfo - var hi Hostinfo - err = db.QueryRow("SELECT hostname, ipaddr, certfp, kernel, "+ - "lastseen, os, os_edition, vendor, model, serialno, clientversion "+ - "FROM hostinfo WHERE hostname=$1", - req.FormValue("h")).Scan(&hi.Hostname, &hi.IPaddr, - &hi.Certfp, &hi.Kernel, &hi.Lastseen, - &hi.OS, &hi.OSEdition, &hi.Vendor, &hi.Model, - &hi.Serialno, &hi.Clientversion) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - tValues["hostinfo"] = hi - - // File and command List - files := make([]string, 0, 0) - commands := make([]string, 0, 0) - rows, err := db.Query("SELECT DISTINCT filename,is_command FROM files "+ - " WHERE certfp=$1 ORDER BY filename", &hi.Certfp) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } else { - defer rows.Close() - for rows.Next() { - var filename sql.NullString - var isCommand sql.NullBool - err = rows.Scan(&filename, &isCommand) - if err != nil && err != sql.ErrNoRows { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - if filename.Valid { - if isCommand.Valid && isCommand.Bool { - commands = append(commands, filename.String) - } else { - files = append(files, filename.String) - } - } - } - } - tValues["files"] = files - tValues["commands"] = commands - } else { - w.Write([]byte("Missing or wrong parameters.")) - return - } - - // Render template - err = templates.ExecuteTemplate(w, templatename, tValues) - if err != nil { - s := "

\nTemplate: " + templatename + "
\n

" + err.Error() + "

" - w.Write([]byte(s)) - } -} diff --git a/server/web/frontpage.go b/server/web/frontpage.go deleted file mode 100644 index 107d69e..0000000 --- a/server/web/frontpage.go +++ /dev/null @@ -1,161 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - "html/template" - "net/http" - "net/http/cgi" - "os" - - "github.com/lib/pq" -) - -var templatePath string -var templates *template.Template -var dbConnectionString string - -type waitingForApproval struct { - Ipaddr sql.NullString - Hostname sql.NullString - Received pq.NullTime -} - -func init() { - http.HandleFunc("/", frontpage) - http.HandleFunc("/search", search) - http.HandleFunc("/browse", browse) -} - -func main() { - if len(os.Args) >= 2 && os.Args[1] == "--dev" { - templatePath = "../templates" - dbConnectionString = "sslmode=disable host=/var/run/postgresql" - fmt.Println("Listening on port 8080") - http.HandleFunc("/static/", staticfiles) - http.ListenAndServe("127.0.0.1:8080", nil) - } else { - templatePath = "/var/www/nivlheim/templates" - dbConnectionString = "dbname=apache host=/var/run/postgresql" - cgi.Serve(nil) - } -} - -func frontpage(w http.ResponseWriter, req *http.Request) { - db, err := sql.Open("postgres", dbConnectionString) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer db.Close() - - if req.FormValue("approve") != "" { - approved := req.FormValue("approve") == "1" - var res sql.Result - res, err = db.Exec("UPDATE waiting_for_approval SET approved=$1 "+ - "WHERE hostname=$2 AND ipaddr=$3", - approved, - req.FormValue("h"), - req.FormValue("ip")) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - var rows int64 - rows, err = res.RowsAffected() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - if rows == 0 { - http.Error(w, "Record not found.", http.StatusNotFound) - return - } - w.WriteHeader(http.StatusOK) - switch approved { - case true: - w.Write([]byte("Approved")) - case false: - w.Write([]byte("Denied")) - } - return - } - - // Load html templates - templates, err := template.ParseGlob(templatePath + "/*") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - machines := make([]string, 0, 0) - rows, err := db.Query("SELECT hostname FROM hostinfo ORDER BY hostname") - if err != nil { - http.Error(w, "1: "+err.Error(), http.StatusInternalServerError) - return - } - - { - defer rows.Close() - for rows.Next() { - var hostname sql.NullString - err = rows.Scan(&hostname) - if err != nil && err != sql.ErrNoRows { - http.Error(w, "2: "+err.Error(), http.StatusInternalServerError) - return - } - if hostname.Valid { - machines = append(machines, hostname.String) - } - } - } - - var filesLastHour int - db.QueryRow("SELECT count(*) FROM files WHERE " + - "received > now() - '1 hour'::INTERVAL").Scan(&filesLastHour) - - var machinesLastHour int - db.QueryRow("SELECT count(distinct certfp) FROM files WHERE " + - "received > now() - '1 hour'::INTERVAL").Scan(&machinesLastHour) - - var totalMachines int - db.QueryRow("SELECT count(*) FROM hostinfo").Scan(&totalMachines) - - approval := make([]waitingForApproval, 0, 0) - rows, err = db.Query("SELECT ipaddr, hostname, received " + - "FROM waiting_for_approval WHERE approved IS NULL ORDER BY hostname") - if err != nil { - http.Error(w, "1: "+err.Error(), http.StatusInternalServerError) - return - } - - { - defer rows.Close() - for rows.Next() { - var app waitingForApproval - err = rows.Scan(&app.Ipaddr, &app.Hostname, &app.Received) - if err != nil && err != sql.ErrNoRows { - http.Error(w, "4: "+err.Error(), http.StatusInternalServerError) - return - } - approval = append(approval, app) - } - } - - // Fill template values - tValues := make(map[string]interface{}) - tValues["machines"] = machines - tValues["filesLastHour"] = filesLastHour - tValues["totalMachines"] = totalMachines - if totalMachines > 0 { - tValues["reportingPercentage"] = (machinesLastHour * 100) / totalMachines - } - tValues["approval"] = approval - - // Render template - templates.ExecuteTemplate(w, "frontpage.html", tValues) -} - -func staticfiles(w http.ResponseWriter, req *http.Request) { - http.ServeFile(w, req, "../static/"+req.URL.Path[8:]) -} diff --git a/server/web/search.go b/server/web/search.go deleted file mode 100644 index e95acf0..0000000 --- a/server/web/search.go +++ /dev/null @@ -1,117 +0,0 @@ -package main - -import ( - "database/sql" - "html/template" - "net/http" - "strings" - - "github.com/lib/pq" -) - -type Hit struct { - Fileid int - Filename string - Excerpt template.HTML - Hostname string - Number int -} - -func search(w http.ResponseWriter, req *http.Request) { - db, err := sql.Open("postgres", dbConnectionString) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer db.Close() - - hits := make([]Hit, 0, 0) - query := strings.TrimSpace(req.FormValue("q")) - lowerq := strings.ToLower(query) - var count int - - if query != "" { - // Number of hits - err = db.QueryRow("SELECT count(distinct(filename,certcn)) FROM files "+ - "WHERE lower(content) LIKE '%' || $1 || '%' ", lowerq).Scan(&count) - if err != nil && err != sql.ErrNoRows { - http.Error(w, "2: "+err.Error(), http.StatusInternalServerError) - return - } - - // Hit list - rows, err := db.Query("SELECT filename,certcn,max(received) "+ - "FROM files "+ - "WHERE lower(content) LIKE '%' || $1 || '%' "+ - "GROUP BY filename,certcn "+ - "ORDER BY max(received) DESC", lowerq) - if err != nil { - http.Error(w, "1: "+err.Error(), http.StatusInternalServerError) - return - } else { - defer rows.Close() - for no := 1; rows.Next(); no++ { - var hit Hit - var received pq.NullTime - err = rows.Scan(&hit.Filename, &hit.Hostname, &received) - if err != nil { - http.Error(w, "3: "+err.Error(), http.StatusInternalServerError) - return - } - hit.Number = no - var content string - err = db.QueryRow("SELECT fileid, content FROM files "+ - "WHERE filename=$1 AND certcn=$2 "+ - "ORDER BY received DESC LIMIT 1", - hit.Filename, hit.Hostname). - Scan(&hit.Fileid, &content) - switch { - case err == sql.ErrNoRows: - continue - case err != nil: - http.Error(w, "2: "+err.Error(), http.StatusInternalServerError) - return - } - i := strings.Index(strings.ToLower(content), lowerq) - start := i - 20 - if start < 0 { - start = 0 - } - end := i + len(query) + 20 - if end >= len(content) { - end = len(content) - 1 - } - ex := content[start:end] - lowerex := strings.ToLower(ex) - i = strings.Index(lowerex, lowerq) - if i > -1 { - i2 := i + len(query) - ex = ex[0:i2] + "
" + ex[i2:] - ex = ex[0:i] + "" + ex[i:] - } - hit.Excerpt = template.HTML(ex) - hits = append(hits, hit) - } - } - } - - // Load templates - templates, err := template.ParseGlob(templatePath + "/*") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // Fill template values - tValues := make(map[string]interface{}) - tValues["q"] = req.FormValue("q") - tValues["hits"] = hits - tValues["count"] = count - - // Render template - err = templates.ExecuteTemplate(w, "searchpage.html", tValues) - if err != nil { - s := "

Error:

" + err.Error() + "

" - w.Write([]byte(s)) - } -} From b68b3928a0d684240b2493c29872e7c90837e417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Hagberg?= Date: Fri, 23 Feb 2018 13:34:39 +0100 Subject: [PATCH 10/16] Download & package 3rd party JS and CSS libraries Also: Compile Handlebars templates during build process Updated test_packages.sh to use the API Fixed API urls for prod/dev in Javascript files --- .gitignore | 4 +-- rpm/nivlheim.spec | 18 +++++++++++- rpm/test_packages.sh | 17 +++++++++--- server/service/taskrunner.go | 4 +++ server/website/browse.html | 12 ++------ server/website/index.html | 12 ++------ server/website/js/browse.js | 6 ++-- server/website/js/functions.js | 4 +++ server/website/js/index.js | 14 +++++----- server/website/js/search.js | 2 +- server/website/libs/download_libraries.sh | 34 +++++++++++++++++++++++ server/website/search.html | 12 ++------ 12 files changed, 91 insertions(+), 48 deletions(-) create mode 100755 server/website/libs/download_libraries.sh diff --git a/.gitignore b/.gitignore index 0eb8849..97e1f2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ *.exe -server/web/web server/service/service -server/website/libs +server/website/libs/* +!server/website/libs/download_libraries.sh diff --git a/rpm/nivlheim.spec b/rpm/nivlheim.spec index 894221d..018b42d 100644 --- a/rpm/nivlheim.spec +++ b/rpm/nivlheim.spec @@ -13,6 +13,7 @@ License: GPLv3+ URL: https://github.com/usit-gd/nivlheim Source0: https://github.com/usit-gd/nivlheim/archive/%{getenv:GIT_BRANCH}.tar.gz +BuildRequires: curl, unzip, npm BuildRequires: perl(Archive::Tar) BuildRequires: perl(Archive::Zip) BuildRequires: perl(CGI) @@ -134,9 +135,24 @@ install -p -m 0644 server/nivlheim.service %{buildroot}%{_unitdir}/%{name}.servi install -p -m 0644 server/logrotate.conf %{buildroot}%{_sysconfdir}/logrotate.d/%{name}-server install -p -m 0755 -D client/cron_hourly %{buildroot}%{_sysconfdir}/cron.hourly/nivlheim_client cp -r server/service %{buildroot}%{_localstatedir}/nivlheim/go/src/ -cp -r server/website/* %{buildroot}%{_localstatedir}/www/html/ echo %{version} > %{buildroot}%{_sysconfdir}/nivlheim/version +# Static website files +cp -r server/website/* %{buildroot}%{_localstatedir}/www/html/ +%{buildroot}%{_localstatedir}/www/html/libs/download_libraries.sh +rm %{buildroot}%{_localstatedir}/www/html/libs/*.sh +rm -rf %{buildroot}%{_localstatedir}/www/html/mockapi + +# Compile web templates +npm install handlebars -g +cd %{buildroot}%{_localstatedir}/www/html/templates +handlebars *.handlebars -min -f templates.js +mv templates.js ../js/ +cd ../libs/ +mv handlebars.runtime.min.js handlebars.min.js +cd %{_builddir} +rm -rf %{buildroot}%{_localstatedir}/www/html/templates + %check perl -c %{buildroot}%{_sbindir}/nivlheim_client perl -c %{buildroot}/var/www/cgi-bin/secure/renewcert diff --git a/rpm/test_packages.sh b/rpm/test_packages.sh index 86aea71..3f63091 100755 --- a/rpm/test_packages.sh +++ b/rpm/test_packages.sh @@ -5,6 +5,7 @@ set -x # It should signal success by outputting "END_TO_END_SUCCESS" if and only if # the test(s) succeeded. +# Install the packages. Different methods on Fedora and CentOS. if [ -f /etc/fedora-release ]; then sudo dnf copr -y enable oyvindh/Nivlheim-test sudo dnf install -y nivlheim-client nivlheim-server || touch installerror @@ -14,20 +15,27 @@ elif [ -f /etc/centos-release ]; then https://copr.fedorainfracloud.org/coprs/oyvindh/Nivlheim-test/repo/epel-7/oyvindh-Nivlheim-test-epel-7.repo sudo yum install -y nivlheim-client nivlheim-server || touch installerror fi - if [ -f installerror ]; then echo "Package installation failed." exit fi -if [ $(curl -s -k https://localhost/ | grep -c "Nivlheim") -eq 0 ]; then +# Check that the home page is being served +if [ $(curl -s -k https://localhost/ | grep -c "Nivlheim") -eq 0 ]; then echo "The web server isn't properly configured and running." exit fi +# Configure the client to use the server at localhost echo "server=localhost" | sudo tee -a /etc/nivlheim/client.conf +# Run the client, it will be put on waiting list for a certificate sudo /usr/sbin/nivlheim_client -sudo -u apache psql -c 'update waiting_for_approval set approved=true;' +# Approve the client, using the API +ID=`curl -s 'http://localhost:4040/api/v0/awaitingApproval?fields=approvalId'|perl -ne 'print $1 if /"approvalId":\s+(\d+)/'` +curl -X PUT -s "http://localhost:4040/api/v0/awaitingApproval/$ID?hostname=abcdef" + +# Run the client again, this time it will receive a certificate +# and post data into the system sudo /usr/sbin/nivlheim_client if [ ! -f /var/nivlheim/my.crt ]; then echo "Certificate generation failed." @@ -38,7 +46,8 @@ fi OK=0 for try in {1..20}; do sleep 3 - if [ $(curl -s -k https://localhost/ | grep -c "novalocal") -gt 0 ]; then + # Query the API for the new machine + if [ $(curl -s 'http://localhost:4040/api/v0/hostlist?fields=hostname' | grep -c "abcdef") -gt 0 ]; then OK=1 break fi diff --git a/server/service/taskrunner.go b/server/service/taskrunner.go index 567c22a..fde1d14 100644 --- a/server/service/taskrunner.go +++ b/server/service/taskrunner.go @@ -69,6 +69,10 @@ func main() { defer log.Println("Stopped.") log.Println("Starting up.") + if devmode { + log.Println("Running in development mode.") + } + // Connect to database var dbConnectionString string if devmode { diff --git a/server/website/browse.html b/server/website/browse.html index 51810d9..709356a 100644 --- a/server/website/browse.html +++ b/server/website/browse.html @@ -5,23 +5,15 @@ Nivlheim - - + + diff --git a/server/website/index.html b/server/website/index.html index eb828ee..b3fdf44 100644 --- a/server/website/index.html +++ b/server/website/index.html @@ -5,23 +5,15 @@ Nivlheim - - + + diff --git a/server/website/js/browse.js b/server/website/js/browse.js index 7eddec2..6dccd88 100644 --- a/server/website/js/browse.js +++ b/server/website/js/browse.js @@ -5,7 +5,7 @@ function browseHost(certfp, pushState = true) { } APIcall( //"mockapi/browsehost.json", - "http://127.0.0.1:4040/api/v0/host?certfp="+encodeURIComponent(certfp)+ + "/api/v0/host?certfp="+encodeURIComponent(certfp)+ "&fields=ipAddress,hostname,lastseen,os,osEdition,"+ "kernel,vendor,model,serialNo,clientVersion,certfp,files", "browsehost", "#placeholder_browse"); @@ -18,7 +18,7 @@ function browseFile(fileId, pushState = true) { } APIcall( //"mockapi/browsefile.json", - "http://127.0.0.1:4040/api/v0/file?fields=lastModified,hostname,filename,"+ + "/api/v0/file?fields=lastModified,hostname,filename,"+ "content,certfp,versions&fileId="+encodeURIComponent(fileId), "browsefile", "#placeholder_browse") .done(function(){ @@ -40,7 +40,7 @@ function browseFile2(hostname, filename, pushState = true) { } APIcall( //"mockapi/browsefile.json", - "http://127.0.0.1:4040/api/v0/file?fields=fileId,lastModified,"+ + "/api/v0/file?fields=fileId,lastModified,"+ "hostname,filename,content,certfp,versions"+ "&filename="+encodeURIComponent(filename)+ "&hostname="+encodeURIComponent(hostname), diff --git a/server/website/js/functions.js b/server/website/js/functions.js index b41d4ec..6f0c610 100644 --- a/server/website/js/functions.js +++ b/server/website/js/functions.js @@ -49,6 +49,10 @@ function renderTemplate(name, templateValues, domElement, deferredObj) { } function APIcall(url, templateName, domElement) { + if (location.origin.match('http://(127\\.0\\.0\\.1|localhost)')) { + // Developer mode. Assumes the API is running locally on port 4040. + url = "http://localhost:4040" + url; + } var deferredObj = $.Deferred(); $.getJSON(url, function(data){ try { diff --git a/server/website/js/index.js b/server/website/js/index.js index b5b97a8..388b33a 100644 --- a/server/website/js/index.js +++ b/server/website/js/index.js @@ -10,28 +10,28 @@ $(document).ready(function(){ APIcall( //"mockapi/systemstatus_data.json", - "http://127.0.0.1:4040/api/v0/status", + "/api/v0/status", "systemstatus", $('#placeholder_systemstatus')); APIcall( //"mockapi/awaiting_approval.json", - "http://127.0.0.1:4040/api/v0/awaitingApproval"+ + "/api/v0/awaitingApproval"+ "?fields=hostname,reversedns,ipaddress,approvalId", "awaiting_approval", $('#placeholder_approval')); APIcall( //"mockapi/latestnewmachines.json", - "http://127.0.0.1:4040/api/v0/hostlist?fields=hostname,certfp,lastseen"+ + "/api/v0/hostlist?fields=hostname,certfp,lastseen"+ "&rsort=lastseen&limit=10", "latestnewmachines", $('#placeholder_latestnewmachines')); }); function approve(id) { $.ajax({ - url : 'http://127.0.0.1:4040/api/v0/awaitingApproval/' + url : '/api/v0/awaitingApproval/' +id+'?hostname='+$('input#hostname'+id).val(), method: "PUT" }) .always(function(){ - APIcall("http://127.0.0.1:4040/api/v0/awaitingApproval"+ + APIcall("/api/v0/awaitingApproval"+ "?fields=hostname,reversedns,ipaddress,approvalId", "awaiting_approval", $('#placeholder_approval')); }); @@ -39,11 +39,11 @@ function approve(id) { function deny(id) { $.ajax({ - url : 'http://127.0.0.1:4040/api/v0/awaitingApproval/'+id, + url : '/api/v0/awaitingApproval/'+id, method: "DELETE" }) .always(function(){ - APIcall("http://127.0.0.1:4040/api/v0/awaitingApproval"+ + APIcall("/api/v0/awaitingApproval"+ "?fields=hostname,reversedns,ipaddress,approvalId", "awaiting_approval", $('#placeholder_approval')); }); diff --git a/server/website/js/search.js b/server/website/js/search.js index 3974e11..eb61e93 100644 --- a/server/website/js/search.js +++ b/server/website/js/search.js @@ -27,7 +27,7 @@ function newSearch() { function performSearch(q) { APIcall( //"mockapi/searchpage.json", - "http://127.0.0.1:4040/api/v0/searchpage?q="+encodeURIComponent(q)+ + "/api/v0/searchpage?q="+encodeURIComponent(q)+ "&page=1&hitsPerPage=10&excerpt=80", "search", "#placeholder_searchresult"); } diff --git a/server/website/libs/download_libraries.sh b/server/website/libs/download_libraries.sh new file mode 100755 index 0000000..2036106 --- /dev/null +++ b/server/website/libs/download_libraries.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# This script downloads 3rd party Javascript and CSS libraries +# that the website uses. +# These libraries do not come with the Git repository, +# but are downloaded during the build process and added to the packages. + +# As a developer, you can run this script to download the libraries +# into the libs/ folder. + + +# +# +# +# +# + +cd `dirname $0` +echo "Downloading Javascript and CSS libraries into `pwd`" + +curl -s --remote-name https://code.jquery.com/jquery-3.3.1.min.js +curl -s --remote-name https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js +curl -s --remote-name https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.runtime.min.js +curl -s --remote-name https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.min.js +curl -s --remote-name https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css +curl -s --remote-name https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css.map + +curl -s --remote-name https://use.fontawesome.com/releases/v5.0.6/fontawesome-free-5.0.6.zip +rm -rf fontawesome-free-5.0.6 fontawesome +unzip -q fontawesome-free-5.0.6.zip +mv fontawesome-free-5.0.6/on-server fontawesome +rm -rf fontawesome-free-5.0.6 fontawesome-free-5.0.6.zip diff --git a/server/website/search.html b/server/website/search.html index 711e541..d94a5df 100644 --- a/server/website/search.html +++ b/server/website/search.html @@ -5,23 +5,15 @@ Nivlheim - - + + From 7e4540d6108576288956ee33dee4a5fd7f4bc11b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Hagberg?= Date: Fri, 23 Feb 2018 20:17:38 +0000 Subject: [PATCH 11/16] Fixed lots of errors in the spec file and setup Fixed npm/handlebars requirement in spec file. Fetching JS and CSS libraries as part of the setup after package installation. That way, we don't distribute them, so we don't have to deal with software licenses. --- rpm/buildrpm.sh | 2 +- rpm/nivlheim.spec | 26 ++++++++--------------- server/setup.sh | 4 ++++ server/website/libs/download_libraries.sh | 15 +++++++------ 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/rpm/buildrpm.sh b/rpm/buildrpm.sh index a3c7f17..121c5ee 100755 --- a/rpm/buildrpm.sh +++ b/rpm/buildrpm.sh @@ -20,7 +20,7 @@ then exit 1 fi cp *.spec $BUILDDIR/SPECS/ -[ -f *.patch ] && cp *.patch $BUILDDIR/SOURCES/ +cp *.patch $BUILDDIR/SOURCES/ 2>/dev/null SPEC=`eval echo $BUILDDIR/SPECS/*.spec` echo "buildrpm: Spec file = $SPEC" diff --git a/rpm/nivlheim.spec b/rpm/nivlheim.spec index 018b42d..5593cf4 100644 --- a/rpm/nivlheim.spec +++ b/rpm/nivlheim.spec @@ -13,7 +13,8 @@ License: GPLv3+ URL: https://github.com/usit-gd/nivlheim Source0: https://github.com/usit-gd/nivlheim/archive/%{getenv:GIT_BRANCH}.tar.gz -BuildRequires: curl, unzip, npm +BuildRequires: curl, unzip +BuildRequires: npm(handlebars) BuildRequires: perl(Archive::Tar) BuildRequires: perl(Archive::Zip) BuildRequires: perl(CGI) @@ -137,21 +138,12 @@ install -p -m 0755 -D client/cron_hourly %{buildroot}%{_sysconfdir}/cron.hourly/ cp -r server/service %{buildroot}%{_localstatedir}/nivlheim/go/src/ echo %{version} > %{buildroot}%{_sysconfdir}/nivlheim/version -# Static website files -cp -r server/website/* %{buildroot}%{_localstatedir}/www/html/ -%{buildroot}%{_localstatedir}/www/html/libs/download_libraries.sh -rm %{buildroot}%{_localstatedir}/www/html/libs/*.sh -rm -rf %{buildroot}%{_localstatedir}/www/html/mockapi - # Compile web templates -npm install handlebars -g -cd %{buildroot}%{_localstatedir}/www/html/templates -handlebars *.handlebars -min -f templates.js -mv templates.js ../js/ -cd ../libs/ -mv handlebars.runtime.min.js handlebars.min.js -cd %{_builddir} -rm -rf %{buildroot}%{_localstatedir}/www/html/templates +handlebars server/website/templates --min -f server/website/js/templates.js + +# Copy static website files, excluding files that are only for development +rm -rf server/website/mockapi server/website/templates +cp -r server/website/* %{buildroot}%{_localstatedir}/www/html/ %check perl -c %{buildroot}%{_sbindir}/nivlheim_client @@ -192,7 +184,7 @@ rm -rf %{buildroot} %dir /var/log/nivlheim /var/www/nivlheim /var/www/cgi-bin -/var/www/html +/var/www/html/* %attr(0644, root, apache) /var/www/nivlheim/log4perl.conf %attr(0755, root, root) %{_localstatedir}/nivlheim/setup.sh %{_localstatedir}/nivlheim @@ -208,7 +200,7 @@ rm -rf %{buildroot} %systemd_postun_with_restart %{name}.service %changelog -* Wed Feb 21 2018 Øyvind Hagberg - 0.1.4-20180221 +* Fri Feb 23 2018 Øyvind Hagberg - 0.1.4-20180223 - New web frontend, installs in /var/www/html. frontpage.cgi is gone. * Fri Jan 05 2018 Øyvind Hagberg - 0.1.1-20180105 diff --git a/server/setup.sh b/server/setup.sh index 22a1de7..e8d37b1 100644 --- a/server/setup.sh +++ b/server/setup.sh @@ -6,6 +6,10 @@ if [ `whoami` != "root" ]; then exit 1 fi +# download 3rd party Javascript and CSS libraries +cd /var/www/html/libs +./download_libraries.sh && rm ./download_libraries.sh + # make dirs mkdir -p /var/www/nivlheim/{db,certs,CA,queue} diff --git a/server/website/libs/download_libraries.sh b/server/website/libs/download_libraries.sh index 2036106..7d4384f 100755 --- a/server/website/libs/download_libraries.sh +++ b/server/website/libs/download_libraries.sh @@ -17,17 +17,18 @@ # # +set -e cd `dirname $0` echo "Downloading Javascript and CSS libraries into `pwd`" -curl -s --remote-name https://code.jquery.com/jquery-3.3.1.min.js -curl -s --remote-name https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js -curl -s --remote-name https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.runtime.min.js -curl -s --remote-name https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.min.js -curl -s --remote-name https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css -curl -s --remote-name https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css.map +curl -sS --remote-name https://code.jquery.com/jquery-3.3.1.min.js +curl -sS --remote-name https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js +curl -sS --remote-name https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.runtime.min.js +curl -sS --remote-name https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.min.js +curl -sS --remote-name https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css +curl -sS --remote-name https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css.map -curl -s --remote-name https://use.fontawesome.com/releases/v5.0.6/fontawesome-free-5.0.6.zip +curl -sS --remote-name https://use.fontawesome.com/releases/v5.0.6/fontawesome-free-5.0.6.zip rm -rf fontawesome-free-5.0.6 fontawesome unzip -q fontawesome-free-5.0.6.zip mv fontawesome-free-5.0.6/on-server fontawesome From 08fbef504ae953ed2141df86abe82091e4d2956a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Hagberg?= Date: Fri, 23 Feb 2018 22:24:31 +0100 Subject: [PATCH 12/16] Fixed problem with SELinux and httpd proxy to API --- rpm/test_packages.sh | 6 ++++++ server/service/api_status.go | 6 +++++- server/setup.sh | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/rpm/test_packages.sh b/rpm/test_packages.sh index 3f63091..d32b91c 100755 --- a/rpm/test_packages.sh +++ b/rpm/test_packages.sh @@ -26,6 +26,12 @@ if [ $(curl -s -k https://localhost/ | grep -c "Nivlheim") -eq 0 exit fi +# Check that the API is available through the main web server +if ! curl -so /dev/null https://localhost/api/v0/status; then + echo "The API is unavailable." + exit +fi + # Configure the client to use the server at localhost echo "server=localhost" | sudo tee -a /etc/nivlheim/client.conf # Run the client, it will be put on waiting list for a certificate diff --git a/server/service/api_status.go b/server/service/api_status.go index 253e614..beb42a7 100644 --- a/server/service/api_status.go +++ b/server/service/api_status.go @@ -33,7 +33,11 @@ func (vars *apiMethodStatus) ServeHTTP(w http.ResponseWriter, req *http.Request) http.Error(w, err.Error(), http.StatusInternalServerError) return } - status.ReportingPercentageLastHour = 100 * machinesLastHour / status.NumOfMachines + if status.NumOfMachines > 0 { + status.ReportingPercentageLastHour = 100 * machinesLastHour / status.NumOfMachines + } else { + status.ReportingPercentageLastHour = 0 + } returnJSON(w, req, status) } diff --git a/server/setup.sh b/server/setup.sh index e8d37b1..b078dd1 100644 --- a/server/setup.sh +++ b/server/setup.sh @@ -56,7 +56,8 @@ chmod 0644 /var/www/nivlheim/default_cert.pem /var/www/nivlheim/CA/nivlheimca.cr chcon -R -t httpd_sys_rw_content_t /var/log/nivlheim /var/www/nivlheim/{db,certs,rand,queue} chown -R apache:apache /var/www/nivlheim/{db,certs,rand,queue} chmod -R u+w /var/www/nivlheim/{db,certs,rand,queue} -setsebool httpd_can_network_connect_db on +setsebool -P httpd_can_network_connect_db on +setsebool -P httpd_can_network_connect on # for proxy connections to the API # initialize postgresql. new/old syntax if ! (/usr/bin/postgresql-setup --initdb || /usr/bin/postgresql-setup initdb); then From 50f1ac0fab3db2f4b6c0919c6cc61f10cb2762a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Hagberg?= Date: Sat, 24 Feb 2018 13:35:45 +0100 Subject: [PATCH 13/16] Fixed faulty curl options in test_packages.sh --- rpm/test_packages.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rpm/test_packages.sh b/rpm/test_packages.sh index d32b91c..ddbed7e 100755 --- a/rpm/test_packages.sh +++ b/rpm/test_packages.sh @@ -21,13 +21,13 @@ if [ -f installerror ]; then fi # Check that the home page is being served -if [ $(curl -s -k https://localhost/ | grep -c "Nivlheim") -eq 0 ]; then +if [ $(curl -sSk https://localhost/ | grep -c "Nivlheim") -eq 0 ]; then echo "The web server isn't properly configured and running." exit fi # Check that the API is available through the main web server -if ! curl -so /dev/null https://localhost/api/v0/status; then +if ! curl -sSko /dev/null https://localhost/api/v0/status; then echo "The API is unavailable." exit fi @@ -37,8 +37,8 @@ echo "server=localhost" | sudo tee -a /etc/nivlheim/client.conf # Run the client, it will be put on waiting list for a certificate sudo /usr/sbin/nivlheim_client # Approve the client, using the API -ID=`curl -s 'http://localhost:4040/api/v0/awaitingApproval?fields=approvalId'|perl -ne 'print $1 if /"approvalId":\s+(\d+)/'` -curl -X PUT -s "http://localhost:4040/api/v0/awaitingApproval/$ID?hostname=abcdef" +ID=`curl -sS 'http://localhost:4040/api/v0/awaitingApproval?fields=approvalId'|perl -ne 'print $1 if /"approvalId":\s+(\d+)/'` +curl -X PUT -sS "http://localhost:4040/api/v0/awaitingApproval/$ID?hostname=abcdef" # Run the client again, this time it will receive a certificate # and post data into the system @@ -53,7 +53,7 @@ OK=0 for try in {1..20}; do sleep 3 # Query the API for the new machine - if [ $(curl -s 'http://localhost:4040/api/v0/hostlist?fields=hostname' | grep -c "abcdef") -gt 0 ]; then + if [ $(curl -sS 'http://localhost:4040/api/v0/hostlist?fields=hostname' | grep -c "abcdef") -gt 0 ]; then OK=1 break fi From 3ce5620feab67cbfeca0fd25f2fa5de1218b54bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Hagberg?= Date: Mon, 26 Feb 2018 08:11:12 +0100 Subject: [PATCH 14/16] test_packages.sh verifies download of js/css libs download_libraries.sh: --prod option downloads runtime handlebars --- rpm/test_packages.sh | 12 ++++++++++-- server/setup.sh | 2 +- server/website/libs/download_libraries.sh | 18 +++++++++++------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/rpm/test_packages.sh b/rpm/test_packages.sh index ddbed7e..cd59a92 100755 --- a/rpm/test_packages.sh +++ b/rpm/test_packages.sh @@ -21,13 +21,21 @@ if [ -f installerror ]; then fi # Check that the home page is being served -if [ $(curl -sSk https://localhost/ | grep -c "Nivlheim") -eq 0 ]; then +if [ $(curl -sSk https://localhost/ | tee /tmp/homepage | grep -c "Nivlheim") -eq 0 ]; then echo "The web server isn't properly configured and running." exit fi +# 3rd party libraries +for URL in $(perl -ne 'm!"(libs/.*?)"!&&print "$1\n"' < /tmp/homepage); +do + if ! curl -sSkfo /dev/null "https://localhost/$URL"; then + echo "The web server returns an error code for $URL" + exit + fi +done # Check that the API is available through the main web server -if ! curl -sSko /dev/null https://localhost/api/v0/status; then +if ! curl -sSkfo /dev/null https://localhost/api/v0/status; then echo "The API is unavailable." exit fi diff --git a/server/setup.sh b/server/setup.sh index b078dd1..696f23d 100644 --- a/server/setup.sh +++ b/server/setup.sh @@ -8,7 +8,7 @@ fi # download 3rd party Javascript and CSS libraries cd /var/www/html/libs -./download_libraries.sh && rm ./download_libraries.sh +./download_libraries.sh --prod && rm ./download_libraries.sh # make dirs mkdir -p /var/www/nivlheim/{db,certs,CA,queue} diff --git a/server/website/libs/download_libraries.sh b/server/website/libs/download_libraries.sh index 7d4384f..820ea32 100755 --- a/server/website/libs/download_libraries.sh +++ b/server/website/libs/download_libraries.sh @@ -21,14 +21,18 @@ set -e cd `dirname $0` echo "Downloading Javascript and CSS libraries into `pwd`" -curl -sS --remote-name https://code.jquery.com/jquery-3.3.1.min.js -curl -sS --remote-name https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js -curl -sS --remote-name https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.runtime.min.js -curl -sS --remote-name https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.min.js -curl -sS --remote-name https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css -curl -sS --remote-name https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css.map +if [[ "$1" == "--prod" ]] { + curl -sSf --remote-name https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.runtime.min.js + mv handlebars.runtime.min.js handlebars.min.js +} else { + curl -sSf --remote-name https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js +} +curl -sSf --remote-name https://code.jquery.com/jquery-3.3.1.min.js +curl -sSf --remote-name https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.min.js +curl -sSf --remote-name https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css +curl -sSf --remote-name https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css.map -curl -sS --remote-name https://use.fontawesome.com/releases/v5.0.6/fontawesome-free-5.0.6.zip +curl -sSf --remote-name https://use.fontawesome.com/releases/v5.0.6/fontawesome-free-5.0.6.zip rm -rf fontawesome-free-5.0.6 fontawesome unzip -q fontawesome-free-5.0.6.zip mv fontawesome-free-5.0.6/on-server fontawesome From 2233b136b12faa780cbb97c2d793cf8adcd0b8be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Hagberg?= Date: Mon, 26 Feb 2018 08:34:23 +0100 Subject: [PATCH 15/16] Fixed a stupid typo in download_libraries.sh --- server/website/libs/download_libraries.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/website/libs/download_libraries.sh b/server/website/libs/download_libraries.sh index 820ea32..7bf89dd 100755 --- a/server/website/libs/download_libraries.sh +++ b/server/website/libs/download_libraries.sh @@ -21,12 +21,12 @@ set -e cd `dirname $0` echo "Downloading Javascript and CSS libraries into `pwd`" -if [[ "$1" == "--prod" ]] { +if [[ "$1" == "--prod" ]]; then curl -sSf --remote-name https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.runtime.min.js mv handlebars.runtime.min.js handlebars.min.js -} else { +else curl -sSf --remote-name https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js -} +fi curl -sSf --remote-name https://code.jquery.com/jquery-3.3.1.min.js curl -sSf --remote-name https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.min.js curl -sSf --remote-name https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css From 024e48939b49582b4471d2cd182c04b3ae934235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Hagberg?= Date: Mon, 26 Feb 2018 09:13:31 +0100 Subject: [PATCH 16/16] Cosmetic changes after code review --- rpm/nivlheim.spec | 1 - server/service/api.go | 3 +-- server/service/api_awaitingApproval.go | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/rpm/nivlheim.spec b/rpm/nivlheim.spec index 5593cf4..cfeb977 100644 --- a/rpm/nivlheim.spec +++ b/rpm/nivlheim.spec @@ -13,7 +13,6 @@ License: GPLv3+ URL: https://github.com/usit-gd/nivlheim Source0: https://github.com/usit-gd/nivlheim/archive/%{getenv:GIT_BRANCH}.tar.gz -BuildRequires: curl, unzip BuildRequires: npm(handlebars) BuildRequires: perl(Archive::Tar) BuildRequires: perl(Archive::Zip) diff --git a/server/service/api.go b/server/service/api.go index 1746476..8b0b4d5 100644 --- a/server/service/api.go +++ b/server/service/api.go @@ -1,6 +1,5 @@ package main -// Create tasks to parse new files that have been read into the database import ( "database/sql" "encoding/json" @@ -67,7 +66,7 @@ func wrapAllowLocalhostCORS(h http.Handler) http.Handler { if req.Method == "OPTIONS" { // When cross-domain, browsers sends OPTIONS first, to check for CORS headers // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS - http.Error(w, "", http.StatusNoContent) + http.Error(w, "", http.StatusNoContent) // 204 OK return } h.ServeHTTP(w, req) diff --git a/server/service/api_awaitingApproval.go b/server/service/api_awaitingApproval.go index c3dfbe1..fde4f2f 100644 --- a/server/service/api_awaitingApproval.go +++ b/server/service/api_awaitingApproval.go @@ -124,5 +124,5 @@ func (vars *apiMethodAwaitingApproval) ServeHTTPREST(w http.ResponseWriter, http.StatusNotFound) return } - http.Error(w, "", http.StatusNoContent) + http.Error(w, "", http.StatusNoContent) // 204 OK }