import algorithm, asyncdispatch, bcrypt, jester, json, jwt, os, osproc, sequtils, strutils, tempfile, times, unittest import logging import ./configuration, ./core, private/util type Worker = object process*: Process workingDir*: string type Session = object user*: UserRef issuedAt*, expires*: Time #const ISO_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss" const JSON = "application/json" proc makeJsonResp(status: HttpCode, details: string = ""): string = result = $(%* { "statusCode": status.int, "status": $status, "details": details }) proc newSession*(user: UserRef): Session = result = Session( user: user, issuedAt: getTime(), expires: daysForward(7).toTime()) proc toJWT*(cfg: StrawBossConfig, session: Session): string = # result = toJWT(%* { # "header": { # "alg": "HS256", # "typ": "JWT" }, # "claims": { # "sub": session.user.name, # "iat": session.issuedAt.toSeconds().int, # "exp": session.expires.toSeconds().int } }) var jwt = JWT( header: JOSEHeader(alg: HS256, typ: "jwt"), claims: toClaims(%*{ "sub": session.user.name, "iat": session.issuedAt.toSeconds().int, "exp": session.expires.toSeconds().int })) jwt.sign(cfg.authSecret) result = $jwt proc fromJWT*(cfg: StrawBossConfig, strTok: string): Session = let jwt = toJWT(strTok) var secret = cfg.authSecret if not jwt.verify(secret): raiseEx "Unable to verify auth token." jwt.verifyTimeClaims() # Find the user record (if authenticated) let username = jwt.claims["sub"].node.str let users = cfg.users.filterIt(it.name == username) if users.len != 1: raiseEx "Could not find session user." result = Session( user: users[0], issuedAt: fromSeconds(jwt.claims["iat"].node.num), expires: fromSeconds(jwt.claims["exp"].node.num)) proc extractSession(cfg: StrawBossConfig, request: Request): Session = # Find the auth header if not request.headers.hasKey("Authorization"): raiseEx "No auth token." # Read and verify the JWT token let headerVal = request.headers["Authorization"] if not headerVal.startsWith("Bearer "): raiseEx "Invalid Authentication type (only 'Bearer' is supported)." result = fromJWT(cfg, headerVal[7..^1]) proc spawnWorker(req: RunRequest): Worker = let dir = mkdtemp() var args = @["run", req.projectName, req.stepName, "-r", req.buildRef, "-w", dir] if req.forceRebuild: args.add("-f") result = Worker( process: startProcess("strawboss", ".", args, loadEnv(), {poUsePath}), workingDir: dir) proc hashPwd*(pwd: string, cost: int8): string = let salt = genSalt(cost) result = hash(pwd, salt) proc validatePwd*(u: UserRef, givenPwd: string): bool = let salt = u.hashedPwd[0..28] # TODO: magic numbers result = compare(u.hashedPwd, hash(givenPwd, salt)) proc makeAuthToken*(cfg: StrawBossConfig, uname, pwd: string): string = if uname == nil or pwd == nil: raiseEx "fields 'username' and 'password' required" # find the user record let users = cfg.users.filterIt(it.name == uname) if users.len != 1: raiseEx "invalid username or password" let user = users[0] if not validatePwd(user, pwd): raiseEx "invalid username or password" result = toJWT(cfg, newSession(user)) template withSession(body: untyped): untyped = var session {.inject.}: Session var authed = false try: session = extractSession(cfg, request) authed = true except: debug "Auth failed: " & getCurrentExceptionMsg() resp(Http401, makeJsonResp(Http401), JSON) if authed: body proc start*(cfg: StrawBossConfig): void = let stopFuture = newFuture[void]() var workers: seq[Worker] = @[] settings: port = Port(8180) appName = "/api" routes: get "/ping": resp($(%*"pong"), JSON) get "/auth-token": try: let authToken = makeAuthToken(cfg, @"username", @"password") resp("\"" & $authToken & "\"", JSON) except: resp(Http401, makeJsonResp(Http401, getCurrentExceptionMsg()), JSON) get "/verify-auth": withSession: resp(Http200, $(%*{ "username": session.user.name }), JSON) get "/projects": withSession: # List project summaries (ProjectDefs only) resp($(%(cfg.projects)), JSON) post "/projects": withSession: # Create a new project definition resp(Http501, makeJsonResp(Http501), JSON) get "/project/@projectName/@version?": withSession: ## Get a detailed project record including step definitions (ProjectConfig). # Make sure we know about that project var project: ProjectDef try: project = cfg.findProject(@"projectName") except: resp(Http404, makeJsonResp(Http404, getCurrentExceptionMsg()), JSON) # Given version var cachedFilePath: string if @"version" != "": cachedFilePath = cfg.artifactsRepo & "/" & project.name & "/configuration." & @"version" & ".json" if not existsFile(cachedFilePath): resp(Http404, makeJsonResp(Http404, "I have never built version " & @"version"), JSON) # No version requested, use "latest" else: let confFilePaths = toSeq(walkFiles("configuration.*.json")) if confFilePaths.len == 0: resp(Http404, makeJsonResp(Http404, "I have not built any versions of " & project.name), JSON) let modTimes = confFilePaths.mapIt(it.getLastModificationTime) cachedFilePath = sorted(zip(confFilePaths, modTimes), proc (a, b: tuple): int = cmp(a.b, b.b))[0].a try: resp(readFile(cachedFilePath), JSON) except: debug "Could not serve cached project configuration at: " & cachedFilePath & "\n\t Reason: " & getCurrentExceptionMsg() resp(Http500, makeJsonResp(Http500, "could not read cached project configuration"), JSON) get "/api/project/@projectName/active": withSession: # List all currently active runs resp(Http501, makeJsonResp(Http501), JSON) get "/api/project/@projectName/@stepName": withSession: # Get step details including runs. resp(Http501, makeJsonResp(Http501), JSON) get "/api/project/@projectName/@stepName/run/@buildRef": withSession: # Get detailed information about a run resp(Http501, makeJsonResp(Http501), JSON) post "/project/@projectName/@stepName/run/@buildRef?": # Kick off a run workers.add(spawnWorker(RunRequest( projectName: @"projectName", stepName: @"stepName", buildRef: if @"buildRef" != "": @"buildRef" else: nil, forceRebuild: false))) # TODO support this with optional query params post "/service/debug/stop": if not cfg.debug: resp(Http404, makeJsonResp(Http404), JSON) else: callSoon(proc(): void = complete(stopFuture)) resp($(%*"shutting down"), JSON) #[ get re".*": resp(Http404, makeJsonResp(Http404), JSON) post re".*": resp(Http404, makeJsonResp(Http404), JSON) ]# waitFor(stopFuture)