Last active
December 13, 2020 22:45
-
-
Save ariankordi/a3dbed15e5da0cea0bfd22b6308a93ce to your computer and use it in GitHub Desktop.
thi this mitmproxy script kinda make me go thonking
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# mitmproxy ctx for logging and http for making responses | |
from mitmproxy import http, ctx | |
# status code constants | |
from mitmproxy.net.http import status_codes | |
# http requests are made for getting and sending back user config | |
import urllib.request, urllib.parse | |
# used for making cookie expire date | |
import datetime | |
# response en/decoding | |
import json | |
from os import getenv | |
# names of hosts for this app | |
app_host = 'incite.educationincites.com' | |
streamservice_host = 'streamservice.educationincites.com' | |
# domain name that cookies are set on | |
cookie_domain = '.educationincites.com' | |
# user agent used for user config requests | |
user_agent = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36' | |
# prefix for authorization header | |
authorization_prefix = 'Bearer {}' | |
# config template that target user will be placed into | |
# most of these are probably not necessary as well as some localstorage keys | |
# todo compare this to the original in some way and see what else is manually added | |
user_config = { | |
# required so that we are not immediately redirected to sso login | |
"bypassedSso": True, | |
"token": " ", | |
# basically forever | |
"tokenExpire": 2000000000000, | |
"tokenValability": 8, | |
"email": "", | |
"userId": "", | |
"username": "", | |
"userLoggedInstaffId": "", | |
"userLoggedIn": " ", | |
"userLoggedInFirstName": "", | |
"userLoggedInLastName": "", | |
"roles": [ | |
{ | |
"_id": "57ae11cc4b39615c060000ec", | |
"role": "district administrator", | |
"active": True | |
} | |
], | |
"role": "district administrator", | |
"allRoles": [ | |
"district administrator" | |
], | |
# not necessary either apparently | |
"defaultCutScores": [], | |
# only present in student response apparently | |
"clientId": "gacobb", | |
"_id": "", | |
"SideBarOn": True, | |
"middleName": "", | |
"inciteAppButton": " ", | |
"inciteAppUrl": " ", | |
"announcements": [], | |
"messages": [], | |
"impersonate": False, | |
# added here, idk if this is actually functional | |
"allowImpersonation": True, | |
"userWhoImpersonatesFirstName": None, | |
"userWhoImpersonatesLastName": None, | |
# LMAO | |
"userWhoImpersonatesFullname": "null null", | |
"userWhoImpersonatesUsername": None, | |
"userWhoImpersonatesstaffId": None, | |
"userWhoImpersonatesId": None, | |
"userWhoImpersonatesRoles": None, | |
"userWhoImpersonatesAllRoles": None, | |
"userWhoImpersonatesRole": None, | |
"userWhoImpersonateshasNoOverallRestriction": True, | |
# added here and enables admin button apparently idk what else it does | |
"hasNoOverallRestriction": True, | |
"districtRestrict": False, | |
"regions": [], | |
"regionsRestrict": True, | |
"schools": [], | |
"schoolsRestrict": True, | |
"classes": [], | |
"classRestrict": True, | |
"targetClassesOnly": True, | |
"links": { | |
"linksTestManagerToPrint": "0", | |
"linksTestManagerToBubblesheets": "0", | |
"linksTestManagerToProctor": "0", | |
"linksTestManagerToManualScoring": "0", | |
"linksTestManagerToEdit": "0", | |
"linksTestManagerToTestScoreEntry": "0", | |
"linksRubricScoringToDashboard": "0" | |
}, | |
"clientInformation": { | |
"clientId": "gacobb", | |
"name": "Cobb County School District", | |
"dbname": "gaCobb", | |
"state": "Georgia", | |
"environment": "production", | |
"isActive": True, | |
"clientStartDate": "2018-06-01T11:51:39.276Z", | |
"services": { | |
"cbt": "https://cbtservice.educationincites.com/api", | |
"proctor": "https://proctorservice.educationincites.com/api", | |
"incite": "https://incitesservice.educationincites.com/api", | |
"inciteClient": "https://incite.educationincites.com/#/client/gacobb" | |
}, | |
"jaspersoftSettings": { | |
"password": "gacobb1*", | |
"server": "https://jaspersoft.education-insights.com/jasperserver-pro", | |
"userName": "incites-gacobb-prod", | |
"brandingImage": "repo:/brandingImages/cobbbubble" | |
}, | |
"currentSchoolYear": "20-21", | |
"schoolStartDate": "Sun Aug 02 2020 04:00:00 GMT+0000 (UTC)", | |
"schoolEndDate": "Thu Jul 01 2021 03:59:59 GMT+0000 (UTC)", | |
"allowSchoolPriorityStandards": True, | |
"partialCreditDefault": { | |
"dropdown": False, | |
"textEntry": False, | |
"fillInTable": False, | |
"hotspot": False, | |
"matching": False, | |
"selectText": False, | |
"numberline": False, | |
"graphingPoint": False, | |
"graphingLine": False, | |
"classification": False, | |
"multiPart": False | |
}, | |
"isProgramsVisible": True, | |
"externalLinks": { | |
"accountButton": { | |
"url": "", | |
"label": "" | |
} | |
} | |
}, | |
"reportSettings": { | |
"teacherReportDataAccess": "teacherSeeOtherTeacherData", | |
"contentTypeRestrictions": [ | |
{ | |
"contentType": "testScoreEntry", | |
"restrictedReports": [ | |
"rpt_assmt_strands_analysis", | |
"rpt_standard_analysis", | |
"rpt_new_item_analysis", | |
"rpt_stud_item_responses", | |
"rpt_student_feedback" | |
] | |
} | |
], | |
"teacherRestrictedTestTypes": [ | |
"SGM" | |
] | |
}, | |
"menuAccess": { | |
"Standards": True, | |
"Dashboard": True, | |
"Reports": True, | |
"More": True, | |
"Items": True, | |
"Assessments": True, | |
"GlobalLeftNav": True, | |
"ItemsCreateItem": True, | |
"AssessmentsTestManager": True, | |
"MoreCourseCatalog": True, | |
"ItemsCreateRubric": True, | |
"MoreStudents": True, | |
"ItemsSearchRubric": True, | |
"ItemsSearch": True, | |
"AssessmentsCreate": True, | |
"AssessmentsSearch": True, | |
"ReportsTeacher": True, | |
"ItemsCreateResource": True, | |
"MoreClasses": True, | |
"ItemsSearchResource": True, | |
"ReportsReports": True, | |
"ReportsInciteAnalytics": True, | |
"MoreStudentGroups": True, | |
"AssessmentsPrograms": True, | |
"MoreGroups": True, | |
"GlobalLeftNavTeach": True, | |
"GlobalLeftNavAssess": True, | |
"GlobalLeftNavHome": True, | |
"GlobalLeftNavAnalyze": True, | |
"GlobalLeftNavProfessionalLearning": True, | |
"GlobalLeftNavResourceLibrary": True, | |
"GlobalLeftNavParent": True, | |
"AssessmentsFPLiteracy": True, | |
"GlobalLeftNavTeachCatalog": True, | |
"GlobalLeftNavTeachResourceLibrary": True, | |
"GlobalLeftNavProfessionalLearningPLCourses": True, | |
"GlobalLeftNavProfessionalLearningMyGrades": True, | |
"GlobalLeftNavTeachMyCourses": True, | |
"GlobalLeftNavTeachResourceLibraryLOR": True, | |
"GlobalLeftNavProfessionalLearningAdminCourses": True | |
}, | |
"hasReportAccess": { | |
"SchoolReports": True, | |
"PerformanceLevelDistribution_SchoolReports": True, | |
"AssessmentComparison_SchoolReports": True, | |
"ItemAnalysis_SchoolReports": True, | |
"StandardAnalysis_SchoolReports": True, | |
"SubgroupComparison_SchoolReports": True, | |
"AssessmentSummary_SchoolReports": True, | |
"AssessmentStrandsAnalysis_SchoolReports": True, | |
"TeacherReports": True, | |
"AdministrationAnalysis_TeacherReports": True, | |
"AssessmentStrandsAnalysis_TeacherReports": True, | |
"PerformanceLevelDistribution_TeacherReports": True, | |
"AssessmentComparison_TeacherReports": True, | |
"AssessmentSummary_TeacherReports": True, | |
"StandardAnalysis_TeacherReports": True, | |
"DistractorAnalysis_TeacherReports": True, | |
"ItemAnalysis_TeacherReports": True, | |
"SubgroupComparison_TeacherReports": True, | |
"AssessmentTrends_TeacherReports": True, | |
"StudentFeedback_TeacherReports": True, | |
"StudentReports": True, | |
"CurrentYearAssessments_StudentReports": True, | |
"CurrentYearSummary_StudentReports": True, | |
"AssessmentSummary_StudentReports": True, | |
"CurrentYearStandards_StudentReports": True | |
}, | |
"isPrintAssessmentActive": False, | |
"isPrintAnswerActive": False, | |
"isPrintBubbleSheetActive": False, | |
"rolesPrintAssessments": [], | |
"userPrintAssessments": [], | |
"rolesPrintAnswerKey": [], | |
"usersPrintAnswerKey": [], | |
"rolesPrintBubbleSheet": [], | |
"resetStudentScoreRolePrivileges": [], | |
"resetStudentScoreUserPrivileges": [], | |
"continueAssessmentRolePrivileges": [], | |
"continueAssessmentUserPrivileges": [], | |
"usersPrintBubbleSheet": [], | |
"studentScoreResponse": "", | |
"isShowStudentResponse": False, | |
"isHideStudentResponse": False, | |
"isShowScoreAfterTest": False, | |
"rolesReportAssessment": [], | |
"RolesResetStudentScore": [], | |
"reportOnAssessmentUserPrivileges": [], | |
"availableAssessmentContentBanks": [ | |
"Preparation/Practice Test", | |
"Professional Learning", | |
"Controlled Assessment/Exam", | |
"Local School Assessment" | |
], | |
"availableItemContentBanksForRead": [], | |
"availableItemContentBanksForEdit": [], | |
"availableItemContentBanksForAdd": [], | |
"reportControls": { | |
"activeReport": "", | |
"reportDir": "", | |
"brandingImg": "", | |
"activeReportUrl": "", | |
"reportType": "", | |
"schoolYear": "20-21", | |
"assessment": None, | |
"studentAssessment": None, | |
"region": None, | |
"school": None, | |
"teacher": None, | |
"class": None, | |
"classes": None, | |
"student": None, | |
"grade": None, | |
"subject": None, | |
"bank": None, | |
"region_noTest": None, | |
"school_noTest": None, | |
"teacher_noTest": None, | |
"multi": { | |
"assessments": None, | |
"assessmentsFilter": None, | |
"assessmentIds": None, | |
"region": None, | |
"school": None, | |
"teacher": None | |
} | |
}, | |
"passages": [], | |
"notRestrictedByAdministrationWindow": False, | |
"allowMultipleGradeSubject": False, | |
"recommendationEngineOn": True, | |
"administrationTypes": [ | |
{ | |
"text": "Pre Learning", | |
"id": 1 | |
}, | |
{ | |
"text": "During Learning", | |
"id": 2 | |
}, | |
{ | |
"text": "Post Learning", | |
"id": 3 | |
} | |
], | |
"priorityStds": [], | |
"editPerformanceBand": True, | |
"isCurriculumModuleAccessible": False, | |
"showTagsItemSelection": False, | |
"signedCookies": {}, | |
"signedCookiesForFiles": {}, | |
"availableContentTypes": [ | |
{ | |
"value": "itemBank", | |
"label": "Item Bank", | |
"selected": True | |
}, | |
{ | |
"value": "external", | |
"label": "External", | |
"selected": False | |
}, | |
{ | |
"value": "testScoreEntry", | |
"label": "Test Score Entry", | |
"selected": False | |
} | |
], | |
"restrictions": { | |
"grade": [], | |
"subject": [], | |
"contentArea": [] | |
}, | |
"resetFiltersTestManagerWhenImpersonate": False, | |
"resetFiltersClassesWhenImpersonate": False, | |
"resetFiltersCourseCatalogWhenImpersonate": False, | |
"resetFiltersSchoolsWhenImpersonate": False, | |
"resetFiltersStaffWhenImpersonate": False, | |
"resetFiltersStudentsWhenImpersonate": False, | |
"resetFiltersReportsAnalyticsWhenImpersonate": False, | |
"leftSidebarOn": True, | |
"studentPortalSidebarOn": True, | |
"navState": None, | |
"teacherStaffSchools": [], | |
"availableItemContentBanksForReadAndCopy": [ | |
"Shared Teacher Bank", | |
"Touchstones" | |
], | |
"shareWithTeacherGroups": True, | |
"shareWithMySchool": True, | |
"allowCreateStudentGroup": True, | |
"userIcon": "img/user-icons/u-teacher.png" | |
} | |
# request handlers to instantly respond to certain requests | |
# also most of these handlers don't capture hostname except for index page | |
# hopefully there isn't toooooo much? confusion | |
def request(flow): | |
global user_config | |
# hack to get token because the app itself won't do it LMAO | |
if flow.request.path == '/_/token': | |
if not user_config['token']: | |
ctx.log.warn('/_/token requested but no token is set, just returning nothing for now') | |
flow.response = http.HTTPResponse.make( | |
status_codes.OK, | |
'', | |
) | |
return | |
flow.response = http.HTTPResponse.make( | |
status_codes.OK, | |
user_config['token'] | |
) | |
# /checkSession?undefined GET | |
elif '/checkSession' in flow.request.path: | |
ctx.log.info('checkSession, returning status:true') | |
# always return status true json | |
flow.response = http.HTTPResponse.make( | |
status_codes.OK, | |
'{"status":true}', | |
{'Content-Type': 'application/json'} | |
) | |
# handle requesting user config by just returning the current user config | |
# note: student portal requests this path without the api part | |
elif 'getUserSessionSettings' in flow.request.path and flow.request.method == 'POST': | |
ctx.log.info('getUserSessionSettings') | |
# warn if token appears to be blank for some reason | |
if len(user_config['token']) < 2: | |
ctx.log.warn('user config token appears to be blank while requesting user session settings did you mean to do this?') | |
user_config_response = json.dumps(user_config) | |
flow.response = http.HTTPResponse.make( | |
status_codes.OK, | |
user_config_response, | |
{ | |
'Content-Type': 'application/json', | |
'Access-Control-Allow-Origin': '*' | |
} | |
) | |
# index handler which will first request user config for cookies that will be set in response handler | |
elif flow.request.host == app_host and flow.request.path == '/': | |
ctx.log.info('requested app index path!') | |
# get user session settings | |
ctx.log.info('requesting getUserSessionSettings w token') | |
# construct urllib request to get settings | |
settings_get_url = 'https://{}/api/getUserSessionSettings'.format(streamservice_host) | |
req = urllib.request.Request(settings_get_url, method='POST') | |
req.add_header('Authorization', authorization_prefix.format(user_config['token'])) | |
req.add_header('User-Agent', user_agent) | |
resp = urllib.request.urlopen(req) | |
# current user config to take cookie attributes from | |
resp_content = resp.read() | |
# if response content is blank | |
if not resp_content: | |
# usually this happens when token is invalid | |
ctx.log.warn('token is invalid..??? please try using a new token') | |
return | |
user_config_current = json.loads(resp_content) | |
ctx.log.info('getUserSessionSettings retrieved and decoded! token is ' + user_config_current['token']) | |
# copy this current token and (valid) signed cookies | |
user_config['token'] = user_config_current['token'] | |
# cooki | |
user_config['signedCookies'] = user_config_current['signedCookies'] | |
user_config['signedCookiesForFiles'] = user_config_current['signedCookies'] | |
# finally, in order for the app to work correctly we need to SET settings | |
ctx.log.info('now sending back user config to setUserSessionSettings') | |
# begin by making the url from the last url. i am so, so sorry for this line | |
settings_set_url = settings_get_url.replace('get', 'set') | |
req_data = urllib.parse.urlencode({'userSettings': json.dumps(user_config)}).encode() | |
req = urllib.request.Request(settings_set_url, req_data) | |
req.add_header('Authorization', authorization_prefix.format(user_config['token'])) | |
req.add_header('User-Agent', user_agent) | |
#print(req.__dict__) | |
# response content isn't required | |
urllib.request.urlopen(req) | |
# by the way user config doesn't actually need to be returned this time | |
# because it is requested later by the app ok | |
return | |
# add user token to every request if it's not empty sorry that this is lazy | |
# not actually necessary anymore because client will just start sending it now | |
# i will uncomment it anyway as a funny fallover | |
if user_config['token'] and len(user_config['token']): | |
flow.request.headers['Authorization'] = authorization_prefix.format(user_config['token']) | |
def response(flow): | |
global user_config | |
# scrape student portal session settings for token | |
#elif | |
if (flow.request.path == '/getUserSessionSettings' or flow.request.path == '/loginPsswd') and flow.request.method == 'POST': | |
ctx.log.info('hello! scraping getUserSessionSettings (or loginpsswd, same thing) from student portal for user config') | |
if funny_token: | |
ctx.log.info('nvm skipping this for student portal because funny token is defined so that will alwayhs be usee') | |
return | |
if not flow.response.status_code == status_codes.OK: | |
ctx.log.info('getUserSessionSettings response skipped because status code is not 200 uh oh') | |
return | |
if flow.response.content: | |
user_config_current = json.loads(flow.response.content) | |
# copy token AND cookies | |
user_config['token'] = user_config_current['token'] | |
user_config['signedCookies'] = user_config_current['signedCookies'] | |
user_config['signedCookiesForFiles'] = user_config_current['signedCookies'] | |
ctx.log.info('stored student portal token hi ' + user_config['token']) | |
else: | |
ctx.log.warn('wtf no response content for student portal session? hmmm...') | |
# index page | |
elif flow.request.path == '/': | |
# replace js on index page that adds some fixes to the app | |
# sets localstorage items which should make it work when not logged in, and | |
# sets token in localstorage and headers so that it's correct | |
flow.response.text = flow.response.text.replace('var _rollbarConfig', ''' | |
// required to function and log in i think (some may not be necessary) | |
localStorage.setItem("id", " "); | |
localStorage.setItem("userId", " "); | |
localStorage.setItem("school", ""); | |
localStorage.setItem("sso", \'{"enabled":false}\'); | |
localStorage.setItem("userLoggedIn", " "); | |
localStorage.setItem("headers", \'{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"}\'); | |
// request token externally outside of the app so we can set it here | |
// this is a bad line it assumes that this request will logically load before everything else | |
// which may actually be fine for the specific use case but this is supposed to | |
// be a solution to something that the developers of the app could fix anyway | |
fetch('/_/token').then(response => response.text()).then(text => { | |
localStorage.setItem("token", text); | |
localStorage.setItem("headers", \'{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8","Authorization":"Bearer \'+text+\'"}\'); | |
}); | |
var _rollbarConfig''') | |
# assign cookies that have been previously retrieved in request handler | |
cookies = [] | |
# set cookie expiry date for two days from now | |
cookies_expiry = (datetime.datetime.utcnow() + datetime.timedelta(days=2)).strftime('%a, %d %b %Y %H:%M:%S GMT') | |
for k in user_config['signedCookies'].keys(): | |
cookies.append( | |
(k, | |
(user_config['signedCookies'][k], [ | |
('Domain', cookie_domain), | |
('Path', '/'), | |
('Expires', cookies_expiry) | |
# httponly appears to not be settable here | |
])) | |
) | |
#print(cookies) | |
flow.response.cookies = cookies | |
# match main js to patch out checksession and idle log out | |
elif '/js/main' in flow.request.path: | |
#print('this IS JS MAIN HHHHFJFBDH') | |
# there could be many ways to go about doing this and i just picked one of them | |
# i wish i could chain these methods on multiple lines but idk how to do that so. sorry for this | |
flow.response.text = flow.response.text.replace('$rootScope.startCheckSessionInterval();', '/*$rootScope.startCheckSessionInterval();*/') | |
flow.response.text = flow.response.text.replace('triggerCheckSession = true', 'triggerCheckSession = false') | |
# replace idletimeout handler with nothing, which does much more than stopping logouts | |
flow.response.text = flow.response.text.replace('IdleTimeout\', ', 'idleTimeout\', function(){return});(function foo(){//') | |
#flow.response.text = 'hi' | |
# main function, set arguments from environment i guess | |
# FUNNY_TOKEN expected to be token and FUNNY_USERNAME is supposed to be username | |
# nvm no more funny_token because i just realized we can get it earlier | |
funny_token = getenv('FUNNY_TOKEN') | |
funny_username = getenv('FUNNY_USERNAME') | |
if not funny_token: | |
print('FUNNY_TOKEN is not set meaning that you will have to go to the student portal so we can see the token thanks (if that sentence just made ANY sense....)') | |
if not funny_username: | |
print('please set FUNNY_USERNAME environment variable (and FUNNY_TOKEN too if you want)') | |
# copy token which is then used in request handler | |
user_config['token'] = funny_token | |
# copy username to various places | |
user_config['username'] = funny_username | |
user_config['userId'] = funny_username | |
# change full name to username so that it appears in app | |
user_config['userLoggedIn'] = funny_username | |
# probably not necessary | |
user_config['_id'] = funny_username |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment