406caf8e54cb7185d003a85423dfdfb16e1512b7
[nonrtric/plt/ranpm.git] / https-server / main.go
1 //  ============LICENSE_START===============================================
2 //  Copyright (C) 2023 Nordix Foundation. All rights reserved.
3 //  ========================================================================
4 //  Licensed under the Apache License, Version 2.0 (the "License");
5 //  you may not use this file except in compliance with the License.
6 //  You may obtain a copy of the License at
7 //
8 //       http://www.apache.org/licenses/LICENSE-2.0
9 //
10 //  Unless required by applicable law or agreed to in writing, software
11 //  distributed under the License is distributed on an "AS IS" BASIS,
12 //  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 //  See the License for the specific language governing permissions and
14 //  limitations under the License.
15 //  ============LICENSE_END=================================================
16 //
17
18 package main
19
20 import (
21         "bufio"
22         "bytes"
23         "compress/gzip"
24         "io"
25         "net/http"
26         "os"
27         "runtime"
28         "strconv"
29         "strings"
30         "time"
31
32         log "github.com/sirupsen/logrus"
33
34         "github.com/gorilla/mux"
35
36         "net/http/pprof"
37 )
38
39 //== Constants ==//
40
41 const https_port = 443
42
43 const shared_files = "/files"
44 const template_files = "/template-files"
45
46 const file_template = "pm-template.xml.gz"
47
48 var always_return_file = os.Getenv("ALWAYS_RETURN")
49 var generated_files_start_time = os.Getenv("GENERATED_FILE_START_TIME")
50 var generated_files_timezone = os.Getenv("GENERATED_FILE_TIMEZONE")
51
52 var unzipped_template = ""
53
54 // Get static file
55 // Returned file is based on configuration
56 // If env ALWAYS_RETURN points to file under "/files", then that file is returned regardless of requested file
57 // Otherwise the requested file is returned if it exists
58 // If the requested file has a file name prefix of "NONEXISTING",then just 404 is returned
59 func files(w http.ResponseWriter, req *http.Request) {
60         start := time.Now()
61         if req.Method != http.MethodGet {
62                 w.WriteHeader(http.StatusMethodNotAllowed)
63                 return
64         }
65
66         vars := mux.Vars(req)
67
68         if id, ok := vars["fileid"]; ok {
69                 if strings.HasPrefix(id, "NONEXISTING") {
70                         w.WriteHeader(http.StatusNotFound)
71                 }
72                 fn := shared_files + "/" + id
73                 if always_return_file != "" {
74                         fn = always_return_file
75                 }
76                 fileBytes, err := os.ReadFile(fn)
77                 if err != nil {
78                         w.WriteHeader(http.StatusNotFound)
79                         return
80                 }
81                 w.WriteHeader(http.StatusOK)
82                 w.Header().Set("Content-Type", "application/octet-stream")
83                 w.Write(fileBytes)
84
85                 log.Info("File retrieval : ", fn, "as ", id, ", time:", time.Since(start).String())
86                 return
87         }
88
89         w.WriteHeader(http.StatusNotFound)
90 }
91
92 // Unzip data from a reader and push to a writer
93 func gunzipReaderToWriter(w io.Writer, data io.Reader) error {
94         gr, err1 := gzip.NewReader(data)
95
96         if err1 != nil {
97                 return err1
98         }
99         defer gr.Close()
100         data2, err2 := io.ReadAll(gr)
101         if err2 != nil {
102                 return err2
103         }
104         _, err3 := w.Write(data2)
105         if err3 != nil {
106                 return err3
107         }
108         return nil
109 }
110
111 // Zip the contents of a byte buffer and push to a writer
112 func gzipWrite(w io.Writer, data *[]byte) error {
113         gw, err1 := gzip.NewWriterLevel(w, gzip.BestSpeed)
114
115         if err1 != nil {
116                 return err1
117         }
118         defer gw.Close()
119         _, err2 := gw.Write(*data)
120         return err2
121 }
122
123 // Get generated file
124 // Returns a file generated from a template
125 // The requested file shall be according to this fileformat:
126 // A<year><month><day>.<hour><minute><timezone>-<hour><minute><timezone>_<nodename>.xml.gz
127 // Example: A20230220.1400+0100-1415+0100_GNODEBX-332.xml.gz
128 // The date and time shall be equal to or later then the date time configured start time in env GENERATED_FILE_START_TIME
129 // If the requested start time is earlier then the configured starttime, 404 is returned
130 // The returned file has nodename, start and end time set according to the requested file.
131 // In addition, the counter values are set to value representing the number of 15 min perioids since the
132 // configured start time
133 func generatedfiles(w http.ResponseWriter, req *http.Request) {
134         start := time.Now()
135         if req.Method != http.MethodGet {
136                 w.WriteHeader(http.StatusMethodNotAllowed)
137                 return
138         }
139
140         vars := mux.Vars(req)
141
142         if id, ok := vars["fileid"]; ok {
143                 if strings.HasPrefix(id, "NONEXISTING") {
144                         w.WriteHeader(http.StatusNotFound)
145                 }
146                 log.Debug("Request generated file:", id)
147                 timezone := "+0000"
148                 if generated_files_timezone != "" {
149                         timezone = generated_files_timezone
150                         log.Debug("Configured timezone: ", timezone)
151                 } else {
152                         log.Debug("Using default timezone: ", timezone)
153                 }
154
155                 fn := template_files + "/" + file_template
156                 if unzipped_template == "" {
157
158                         var buf3 bytes.Buffer
159                         file, err := os.Open(fn)
160                         if err != nil {
161                                 log.Error("PM template file", file_template, " does not exist")
162                                 w.WriteHeader(http.StatusInternalServerError)
163                                 return
164                         }
165                         errb := gunzipReaderToWriter(&buf3, bufio.NewReader(file))
166                         if errb != nil {
167                                 log.Error("Cannot gunzip file ", file_template, " - ", errb)
168                                 return
169                         }
170
171                         unzipped_template = string(buf3.Bytes())
172
173                 }
174
175                 //Parse file start date/time
176                 //Example: 20230220.1300
177
178                 layout := "20060102.1504"
179                 tref, err := time.Parse(layout, generated_files_start_time)
180                 if err != nil {
181                         log.Error("Env var GENERATED_FILE_START_TIME cannot be parsed")
182                         w.WriteHeader(http.StatusInternalServerError)
183                         return
184                 }
185
186                 //Parse file name start date/time
187
188                 ts := id[1:14]
189                 tcur, err := time.Parse(layout, ts)
190                 if err != nil {
191                         log.Error("File start date/time cannot be parsed: ", ts)
192                         w.WriteHeader(http.StatusInternalServerError)
193                         return
194                 }
195
196                 // Calculate file index based of reference time
197                 file_index := (tcur.Unix() - tref.Unix()) / 900
198                 if file_index < 0 {
199                         log.Error("File start date/time before value of env var GENERATED_FILE_START_TIME :", generated_files_start_time)
200                         w.WriteHeader(http.StatusInternalServerError)
201                         return
202                 }
203
204                 //begintime/endtime format: 2022-04-18T19:00:00+00:00
205                 begintime := tcur.Format("2006-01-02T15:04") + ":00" + timezone
206                 d := time.Duration(900 * time.Second)
207                 tend := tcur.Add(d)
208                 endtime := tend.Format("2006-01-02T15:04") + ":00" + timezone
209
210                 //Extract nodename
211                 nodename := id[30:]
212                 nodename = strings.Split(nodename, ".")[0]
213
214                 template_string := strings.Clone(unzipped_template)
215
216                 log.Debug("Replacing BEGINTIME with: ", begintime)
217                 log.Debug("Replacing ENDTIME with: ", endtime)
218                 log.Debug("Replacing CTR_VALUE with: ", strconv.Itoa(int(file_index)))
219                 log.Debug("Replacing NODE_NAME with: ", nodename)
220
221                 template_string = strings.Replace(template_string, "BEGINTIME", begintime, -1)
222                 template_string = strings.Replace(template_string, "ENDTIME", endtime, -1)
223
224                 template_string = strings.Replace(template_string, "CTR_VALUE", strconv.Itoa(int(file_index)), -1)
225
226                 template_string = strings.Replace(template_string, "NODE_NAME", nodename, -1)
227
228                 b := []byte(template_string)
229                 var buf bytes.Buffer
230                 err = gzipWrite(&buf, &b)
231                 if err != nil {
232                         log.Error("Cannot gzip file ", id, " - ", err)
233                         return
234                 }
235
236                 w.WriteHeader(http.StatusOK)
237                 w.Header().Set("Content-Type", "application/octet-stream")
238                 w.Write(buf.Bytes())
239
240                 log.Info("File retrieval generated file: ", fn, "as ", id, ", time:", time.Since(start).String())
241                 return
242         }
243
244         w.WriteHeader(http.StatusNotFound)
245 }
246
247 // Simple alive check
248 func alive(w http.ResponseWriter, req *http.Request) {
249         //Alive check
250 }
251
252 // == Main ==//
253 func main() {
254
255         log.SetLevel(log.InfoLevel)
256         //log.SetLevel(log.TraceLevel)
257
258         log.Info("Server starting...")
259
260         rtr := mux.NewRouter()
261         rtr.HandleFunc("/files/{fileid}", files)
262         rtr.HandleFunc("/generatedfiles/{fileid}", generatedfiles)
263         rtr.HandleFunc("/", alive)
264
265         rtr.HandleFunc("/custom_debug_path/profile", pprof.Profile)
266
267         http.Handle("/", rtr)
268
269         // Run https
270         log.Info("Starting https service...")
271         err := http.ListenAndServeTLS(":"+strconv.Itoa(https_port), "certs/server.crt", "certs/server.key", nil)
272         if err != nil {
273                 log.Fatal("Cannot setup listener on https: ", err)
274         }
275
276         //Wait until all go routines has exited
277         runtime.Goexit()
278
279         log.Warn("main routine exit")
280         log.Warn("server i stopping...")
281 }