about summary refs log blame commit diff stats
path: root/src/io/request.nim
blob: 6b89eb6b1a68a112afc7db0895b28760a6804f96 (plain) (tree)
1
2
3
4
5
6
7
8
9

              
               

             
                       
                 
               
                    
                     

                












                            




                               

                            



















                                   

                         


                               

                       


                                       

    

                           

                           
                               

                             
                 



                                               
                   
 




                                        
                     
 

                                                      
 
                                   
                                                                


                         

                                                     



                             










































                                                                              


                                                          


                                  






                                           
                                                                         
                                                                              
                                                  

                                                                      




                           
                         

                                     

                             

   
                                                 
                                                             

                                                                            
             
                       


                      
                                                                              
 










                                                                                                                                 
 
                                  
                                                                
                                                         






















                                                                           
                                                  

                                  






                                                                
                                                      
                          
                                                 


                                            
                                                                         
                                                          
                               




                                                                           

                                                               











                                 
 






                                                                  
                                        
                           
import options
import streams
import strutils
import tables

import bindings/quickjs
import io/headers
import js/error
import js/javascript
import types/formdata
import types/url

type
  HttpMethod* = enum
    HTTP_CONNECT = "CONNECT"
    HTTP_DELETE = "DELETE"
    HTTP_GET = "GET"
    HTTP_HEAD = "HEAD"
    HTTP_OPTIONS = "OPTIONS"
    HTTP_PATCH = "PATCH"
    HTTP_POST = "POST"
    HTTP_PUT = "PUT"
    HTTP_TRACE = "TRACE"

  RequestMode* = enum
    NO_CORS = "no-cors"
    SAME_ORIGIN = "same-origin"
    CORS = "cors"
    NAVIGATE = "navigate"
    WEBSOCKET = "websocket"

  RequestDestination* = enum
    NO_DESTINATION = ""
    AUDIO = "audio"
    AUDIOWORKLET = "audioworklet"
    DOCUMENT = "document"
    EMBED = "embed"
    FONT = "font"
    FRAME = "frame"
    IFRAME = "iframe"
    IMAGE = "image"
    MANIFEST = "manifest"
    OBJECT = "object"
    PAINTWORKLET = "paintworklet"
    REPORT = "report"
    SCRIPT = "script"
    SERVICEWORKER = "serviceworker"
    SHAREDWORKER = "sharedworker"
    STYLE = "style"
    TRACK = "track"
    WORKER = "worker"
    XSLT = "xslt"

  CredentialsMode* = enum
    SAME_ORIGIN = "same-origin"
    OMIT = "omit"
    INCLUDE = "include"

  CORSAttribute* = enum
    NO_CORS = "no-cors"
    ANONYMOUS = "anonymous"
    USE_CREDENTIALS = "use-credentials"

type
  Request* = ref RequestObj
  RequestObj* = object
    httpmethod*: HttpMethod
    url*: Url
    headers* {.jsget.}: Headers
    body*: Opt[string]
    multipart*: Opt[FormData]
    referer*: URL
    mode* {.jsget.}: RequestMode
    destination* {.jsget.}: RequestDestination
    credentialsMode* {.jsget.}: CredentialsMode
    proxy*: URL #TODO do something with this
    canredir*: bool
 
  ReadableStream* = ref object of Stream
    isource*: Stream
    buf: string
    isend: bool

jsDestructor(Request)

proc js_url(this: Request): string {.jsfget: "url".} =
  return $this.url

#TODO pretty sure this is incorrect
proc js_referrer(this: Request): string {.jsfget: "referrer".} =
  if this.referer != nil:
    return $this.referer
  return ""

iterator pairs*(headers: Headers): (string, string) =
  for k, vs in headers.table:
    for v in vs:
      yield (k, v)

proc rsReadData(s: Stream, buffer: pointer, bufLen: int): int =
  var s = ReadableStream(s)
  if s.atEnd:
    return 0
  while s.buf.len < bufLen:
    var len: int
    s.isource.read(len)
    if len == 0:
      result = s.buf.len
      copyMem(buffer, addr(s.buf[0]), result)
      s.buf = s.buf.substr(result)
      s.isend = true
      return
    var nbuf: string
    s.isource.readStr(len, nbuf)
    s.buf &= nbuf
  assert s.buf.len >= bufLen
  result = bufLen
  copyMem(buffer, addr(s.buf[0]), result)
  s.buf = s.buf.substr(result)
  if s.buf.len == 0:
    var len: int
    s.isource.read(len)
    if len == 0:
      s.isend = true
    else:
      s.isource.readStr(len, s.buf)

proc rsAtEnd(s: Stream): bool =
  ReadableStream(s).isend

proc rsClose(s: Stream) = {.cast(tags: [WriteIOEffect]).}: #TODO TODO TODO ew.
  var s = ReadableStream(s)
  if s.isend: return
  s.buf = ""
  while true:
    var len: int
    s.isource.read(len)
    if len == 0:
      s.isend = true
      break
    s.isource.setPosition(s.isource.getPosition() + len)

proc newReadableStream*(isource: Stream): ReadableStream =
  new(result)
  result.isource = isource
  result.readDataImpl = rsReadData
  result.atEndImpl = rsAtEnd
  result.closeImpl = rsClose
  var len: int
  result.isource.read(len)
  if len == 0:
    result.isend = true
  else:
    result.isource.readStr(len, result.buf)

func newRequest*(url: URL, httpmethod = HTTP_GET, headers = newHeaders(),
    body = opt(string), multipart = opt(FormData), mode = RequestMode.NO_CORS,
    credentialsMode = CredentialsMode.SAME_ORIGIN,
    destination = RequestDestination.NO_DESTINATION, proxy: URL = nil,
    canredir = false): Request =
  return Request(
    url: url,
    httpmethod: httpmethod,
    headers: headers,
    body: body,
    multipart: multipart,
    mode: mode,
    credentialsMode: credentialsMode,
    destination: destination,
    proxy: proxy
  )

func newRequest*(url: URL, httpmethod = HTTP_GET,
    headers: seq[(string, string)] = @[], body = opt(string),
    multipart = opt(FormData), mode = RequestMode.NO_CORS, proxy: URL = nil,
    canredir = false):
    Request =
  let hl = newHeaders()
  for pair in headers:
    let (k, v) = pair
    hl.table[k] = @[v]
  return newRequest(url, httpmethod, hl, body, multipart, mode, proxy = proxy)

func createPotentialCORSRequest*(url: URL, destination: RequestDestination, cors: CORSAttribute, fallbackFlag = false): Request =
  var mode = if cors == NO_CORS:
    RequestMode.NO_CORS
  else:
    RequestMode.CORS
  if fallbackFlag and mode == NO_CORS:
    mode = SAME_ORIGIN
  let credentialsMode = if cors == ANONYMOUS:
    CredentialsMode.SAME_ORIGIN
  else: CredentialsMode.INCLUDE
  return newRequest(url, destination = destination, mode = mode, credentialsMode = credentialsMode)

#TODO init as an actual dictionary
func newRequest*[T: string|Request](ctx: JSContext, resource: T,
    init = none(JSValue)): JSResult[Request] {.jsctor.} =
  when T is string:
    let url = ?newURL(resource)
    if url.username != "" or url.password != "":
      return err(newTypeError("Input URL contains a username or password"))
    var httpMethod = HTTP_GET
    var headers = newHeaders()
    let referer: URL = nil
    var credentials = CredentialsMode.SAME_ORIGIN
    var body: Opt[string]
    var multipart: Opt[FormData]
    var proxyUrl: URL #TODO?
    let fallbackMode = opt(RequestMode.CORS)
  else:
    let url = resource.url
    var httpMethod = resource.httpMethod
    var headers = resource.headers.clone()
    let referer = resource.referer
    var credentials = resource.credentialsMode
    var body = resource.body
    var multipart = resource.multipart
    var proxyUrl = resource.proxy #TODO?
    let fallbackMode = opt(RequestMode)
    #TODO window
  var mode = fallbackMode.get(RequestMode.NO_CORS)
  let destination = NO_DESTINATION
  #TODO origin, window
  if init.isSome:
    let init = init.get
    httpMethod = fromJS[HttpMethod](ctx,
      JS_GetPropertyStr(ctx, init, "method")).get(HTTP_GET)
    let bodyProp = JS_GetPropertyStr(ctx, init, "body")
    if not JS_IsNull(bodyProp) and not JS_IsUndefined(bodyProp):
      # ????
      multipart = opt(fromJS[FormData](ctx, bodyProp))
      if multipart.isNone:
        body = opt(fromJS[string](ctx, bodyProp))
    #TODO inputbody
    if (multipart.isSome or body.isSome) and
        httpMethod in {HTTP_GET, HTTP_HEAD}:
      return err(newTypeError("HEAD or GET Request cannot have a body."))
    let jheaders = JS_GetPropertyStr(ctx, init, "headers")
    headers.fill(ctx, jheaders)
    credentials = fromJS[CredentialsMode](ctx, JS_GetPropertyStr(ctx, init,
      "credentials")).get(credentials)
    mode = fromJS[RequestMode](ctx, JS_GetPropertyStr(ctx, init, "mode"))
      .get(mode)
    #TODO find a standard compatible way to implement this
    let proxyUrlProp = JS_GetPropertyStr(ctx, init, "proxyUrl")
    proxyUrl = fromJS[URL](ctx, proxyUrlProp).get(nil)
  return ok(Request(
    url: url,
    httpmethod: httpmethod,
    headers: headers,
    body: body,
    multipart: multipart,
    mode: mode,
    credentialsMode: credentials,
    destination: destination,
    proxy: proxyUrl,
    referer: referer
  ))

func credentialsMode*(attribute: CORSAttribute): CredentialsMode =
  case attribute
  of NO_CORS, ANONYMOUS:
    return SAME_ORIGIN
  of USE_CREDENTIALS:
    return INCLUDE

proc addRequestModule*(ctx: JSContext) =
  ctx.registerType(Request)