Implement a command-line interface to the running webserver, and a way to get/set...
[ndcode_site.git] / _config / Session.mjs
1 import assert from 'assert'
2 import fsPromises from 'fs/promises'
3 import http from 'http'
4 import https from 'https'
5 import stream_buffers from 'stream-buffers'
6 import Problem from './Problem.mjs'
7 import XDate from 'xdate'
8
9 class Session {
10   constructor(path) {
11     this.path = path
12     this.persistent = {url: 'http://localhost:8080', cookies: {}}
13   }
14
15   async load() {
16     let buffer
17     try {
18       buffer = await fsPromises.readFile(this.path)
19     }
20     catch (error) {
21       if (error.code !== 'ENOENT')
22         throw error
23
24       try {
25         await fsPromises.rename(this.path + '.new', this.path)
26       }
27       catch (error1) {
28         throw error // ENOENT will make more sense to the user
29       }
30
31       buffer = await fsPromises.readFile(this.path)
32     }
33     this.persistent = JSON.parse(buffer.toString('utf-8'))
34   }
35
36   async save() {
37     await fsPromises.writeFile(
38       this.path + '.new',
39       Buffer.from(JSON.stringify(this.persistent, null, 2) + '\n', 'utf-8')
40     )
41     try {
42       await fsPromises.unlink(this.path)
43     }
44     catch (error) {
45       if (error.code !== 'ENOENT')
46         throw error
47     }
48     await fsPromises.rename(this.path + '.new', this.path)
49   }
50
51   async api_call(endpoint, ...args) {
52     let url = new URL(this.persistent.url + endpoint)
53
54     let http_or_https, default_port
55     if (url.protocol === 'https:') {
56       http_or_https = https
57       default_port = 443
58     }
59     else {
60       http_or_https = http
61       default_port = 80
62     }
63
64     let buffer = Buffer.from(
65       JSON.stringify(args) + '\n',
66       'utf-8'
67     )
68
69     let headers = {
70       'Content-Type': 'application/json',
71       'Content-Length': buffer.length
72     }
73
74     let cookies = []
75     let now = XDate.now()
76     for (let i in this.persistent.cookies) {
77       let cookie = this.persistent.cookies[i]
78
79       let expires = cookie.expires
80       if (expires !== undefined && now >= new XDate(expires).getTime())
81         continue
82
83       let path = cookie.path
84       if (path !== undefined) {
85         if (path.slice(-1) != '/')
86           path += '/'
87         if (url.pathname.slice(0, path.length) !== path)
88           continue
89       }
90
91       cookies.push(`${i}=${cookie.value}`)
92     }
93     if (cookies.length)
94       headers.Cookie = cookies.join(', ')
95
96     let response = await new Promise(
97       (resolve, reject) => {
98         let request = http_or_https.request(
99           {
100             hostname: url.hostname,
101             port: url.port.length === 0 ? default_port : parseInt(url.port),
102             path: url.pathname,
103             method: 'POST',
104             headers: headers
105           },
106           response => {resolve(response)}
107         )
108         request.on('error', error => {reject(error)})
109         request.write(buffer)
110         request.end()
111       }
112     )
113
114     let response_cookies = response.headers['set-cookie'] || []
115     for (let i = 0; i < response_cookies.length; ++i) {
116       let fields = response_cookies[i].split(';')
117       assert(fields.length >= 1)
118
119       let j = fields[0].indexOf('=')
120       assert(j >= 0)
121       let name = fields[0].slice(0, j).trim()
122       let value = fields[0].slice(j + 1).trim()
123       if (
124         value.length >= 2 &&
125         value.charAt(0) === 0x22 &&
126           value.charAt(value.length - 1) === 0x22
127       )
128         value = value.slice(1, -1)
129       let attrs = {value}
130
131       for (/*let*/ j = 1; j < fields.length; ++j) {
132         let k = fields[j].indexOf('=')
133         let attr/*, value*/
134         if (k >= 0) {
135           attr = fields[j].slice(0, k).trim()
136           value = fields[j].slice(k + 1).trim()
137         }
138         else {
139           attr = fields[j].trim()
140           if (attr.length === 0)
141             continue
142           value = null
143         }
144         attr = attr.toLowerCase()
145         assert(
146           attr === 'expires' ||
147             attr === 'max-age' ||
148             attr === 'domain' ||
149             attr === 'path' ||
150             attr === 'secure' ||
151             attr === 'httponly' ||
152             attr === 'samesite'
153         ) // cannot be 'value'
154         attrs[attr] = value
155       }
156
157       let max_age = attrs['max-age']
158       if (max_age !== undefined) {
159         let expires = new XDate()
160         expires.addSeconds(parseInt(max_age))
161         attrs['expires'] = expires.toUTCString()
162         delete attrs['max-age']
163       }
164
165       this.persistent.cookies[name] = attrs
166     }
167
168     let write_stream = new stream_buffers.WritableStreamBuffer()
169     let data = new Promise(
170       (resolve, reject) => {
171         write_stream.
172         on('finish', () => {resolve(write_stream.getContents())}).
173         on('error', () => {reject()})
174       }
175     )
176     response.pipe(write_stream)
177
178     let result = JSON.parse((await data).toString('utf-8'))
179     if (response.statusCode < 200 || response.statusCode >= 300)
180       throw new Problem(result.title, result.detail, result.status)
181     return result
182   }
183 }
184
185 export default Session